Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
134 commits
Select commit Hold shift + click to select a range
61b93da
initial
cschu Dec 15, 2024
6c80f0a
version
cschu Dec 15, 2024
2be12b4
fix: getitem implementation
cschu Dec 15, 2024
e143eb3
fix?: missing counts
cschu Dec 16, 2024
32ce2bd
fix: fixing AlignmentCounter __getitem__/__setitem__ methods
cschu Dec 16, 2024
ed0eaab
merge uniq/ambig seqcounters
cschu Dec 16, 2024
6c32072
fix: AlignmentCounter.has_ambig_counts(), CountManager.has_ambig_coun…
cschu Dec 16, 2024
28f84e8
fix: minor
cschu Dec 16, 2024
b344e9f
updating count annotation
cschu Dec 19, 2024
5ed8cfc
fixed import
cschu Dec 19, 2024
a92722a
added debug message
cschu Dec 19, 2024
515e71f
added debug message
cschu Dec 19, 2024
6efbca6
fixing empty length vector issue?
cschu Dec 19, 2024
ef34cde
fixing empty length vector issue?
cschu Dec 19, 2024
aa09c1a
added debug message
cschu Dec 19, 2024
d3649c2
fixing empty total counts?
cschu Dec 19, 2024
e3bee67
fixing total count issue?
cschu Dec 19, 2024
b466381
debug messaging
cschu Dec 20, 2024
8249d9f
fixed gene writing?
cschu Dec 20, 2024
7fb5c56
fixed gene writing?
cschu Dec 20, 2024
d2226ac
fixed gene writing?
cschu Dec 20, 2024
a1e54c2
fixed gene writing?
cschu Dec 20, 2024
f336493
fixed gene writing?
cschu Dec 21, 2024
cb9ea29
dump seqcounters for debugging
cschu Dec 21, 2024
6ae5c5a
dump seqcounters for debugging
cschu Dec 21, 2024
fbb6a0e
dump seqcounters for debugging
cschu Dec 21, 2024
a70f1a8
changed strand specific order
cschu Dec 21, 2024
bb13e14
debug log
cschu Dec 21, 2024
4579aff
debug log
cschu Dec 21, 2024
a5f7fb1
debug log
cschu Dec 21, 2024
68fa194
fixed gene writing?
cschu Dec 21, 2024
313a061
fixed gene writing?
cschu Dec 21, 2024
e917fe2
starting to replace CountManager
cschu Dec 21, 2024
37b624e
pleasing linters
cschu Dec 22, 2024
9e078ce
removed seq_counter.py
cschu Dec 22, 2024
7976a4d
removed Unique- and AmbiguousRegionCounter classes
cschu Dec 22, 2024
f754653
updated alignment_counter, removed alignment_counter2
cschu Dec 22, 2024
26fbb55
throwing out old code, splitting of regioncount_annotator
cschu Dec 22, 2024
bd1436f
removed count_manager
cschu Dec 22, 2024
b460ebd
removed count_manager references
cschu Dec 22, 2024
1789551
modified gene_count write behaviour in prep of ggroup annotation
cschu Dec 23, 2024
fdd7aaf
modified gene_count write behaviour in prep of ggroup annotation
cschu Dec 23, 2024
4af2f14
change gene group handling during annotation
cschu Dec 23, 2024
39ebd15
change gene group handling during annotation
cschu Dec 23, 2024
c0b4664
change gene group handling during annotation
cschu Dec 23, 2024
b935a24
added debug messaging
cschu Dec 24, 2024
69709f8
solved?
cschu Dec 24, 2024
62c0a52
disabling various logger calls
cschu Dec 24, 2024
2239d29
disabling various logger calls
cschu Dec 24, 2024
413684a
trying to update feature count processing
cschu Dec 25, 2024
1b5cf9c
trying to update feature count processing
cschu Dec 25, 2024
cb6ac1a
trying to update feature count processing
cschu Dec 25, 2024
8977c4d
trying to update feature count processing
cschu Dec 25, 2024
f96e1b3
trying to update feature count processing
cschu Dec 25, 2024
b4fb4d8
trying to update feature count processing
cschu Dec 25, 2024
3594dff
trying to update feature count processing
cschu Dec 25, 2024
0dbd4ae
trying to update feature count processing
cschu Dec 25, 2024
7b8e6bf
debug log
cschu Dec 25, 2024
563b402
trying to fix annotate2
cschu Dec 25, 2024
e9e1715
turn off annotate2 log
cschu Dec 25, 2024
07c6743
trying to update feature count processing
cschu Dec 25, 2024
da8e93b
trying to update feature count processing
cschu Dec 25, 2024
a94d9e7
trying to update feature count processing
cschu Dec 26, 2024
164cc6d
trying to update feature count processing
cschu Dec 26, 2024
0cdf49a
trying to update feature count processing
cschu Dec 26, 2024
3b2b5cb
trying to update feature count processing
cschu Dec 27, 2024
cb9934e
added category scaling comment
cschu Dec 27, 2024
4e119a4
linting + obsolete code removal
cschu Dec 27, 2024
90eb4b5
trying to optimise scaling factors, temp. disabled feature counts
cschu Dec 29, 2024
5b0286f
trying to optimise scaling factors, temp. disabled feature counts
cschu Dec 30, 2024
5baafe0
trying to optimise scaling factors, temp. disabled feature counts
cschu Dec 30, 2024
49e11e3
re-enable feature counts
cschu Dec 30, 2024
b39b4b1
trying to fix scaling factor issue
cschu Dec 30, 2024
b76f168
trying to fix scaling factor issue
cschu Dec 30, 2024
9920404
trying to implement count matrix
cschu Dec 31, 2024
945cf8e
trying to implement count matrix
cschu Dec 31, 2024
07f85f0
trying to implement count matrix
cschu Dec 31, 2024
dbb36da
trying to implement count matrix
cschu Dec 31, 2024
b281a16
trying to implement count matrix
cschu Dec 31, 2024
0b64813
trying to implement count matrix
cschu Dec 31, 2024
7f93e35
trying to implement count matrix
cschu Dec 31, 2024
b8bdf08
trying to implement count matrix
cschu Dec 31, 2024
85d1def
trying to implement count matrix
cschu Dec 31, 2024
e325127
trying to implement count matrix
cschu Dec 31, 2024
c1c93d1
trying to implement count matrix
cschu Dec 31, 2024
21fd963
reactivated feature output
cschu Jan 1, 2025
a47b87e
reactivated feature output
cschu Jan 1, 2025
7723314
reactivated feature output
cschu Jan 1, 2025
27e9e38
reactivated feature output
cschu Jan 1, 2025
2f938f6
reactivated feature output
cschu Jan 1, 2025
03a4a96
reactivated feature output
cschu Jan 1, 2025
74c2992
reactivated feature output
cschu Jan 1, 2025
26a94d8
reactivated feature output
cschu Jan 1, 2025
fb73a0a
reactivated feature output
cschu Jan 1, 2025
d1a2720
reactivated feature output
cschu Jan 1, 2025
d54e3f0
reactivated feature output
cschu Jan 1, 2025
44a773d
reactivated feature output
cschu Jan 1, 2025
6b06a52
reactivated feature output
cschu Jan 1, 2025
a0d1032
reactivated feature output
cschu Jan 1, 2025
8a56e33
pleasing linters, cleanup
cschu Jan 1, 2025
98c6cf4
trying to reduce memory footprint
cschu Jan 2, 2025
7fa8339
trying to reduce memory footprint
cschu Jan 2, 2025
3fde96f
trying to reduce memory footprint
cschu Jan 2, 2025
64845ec
trying to reduce memory footprint
cschu Jan 2, 2025
4745be9
trying to reduce memory footprint
cschu Jan 2, 2025
fa19a80
trying to reduce memory footprint
cschu Jan 2, 2025
54b88a5
trying to reduce memory footprint
cschu Jan 2, 2025
3d4a4f5
trying to reduce memory footprint
cschu Jan 2, 2025
2ffa0c5
trying category-wise processing
cschu Jan 2, 2025
688a081
trying category-wise processing
cschu Jan 2, 2025
64f8149
trying category-wise processing
cschu Jan 2, 2025
f7c16c3
truncated unannotated hash
cschu Jan 3, 2025
2fe0bdc
making adjustments for new db format
cschu Jan 7, 2025
9a0f888
making adjustments for new db format
cschu Jan 7, 2025
b51b48d
making adjustments for new db format
cschu Jan 7, 2025
0e1edae
making adjustments for new db format
cschu Jan 7, 2025
c065163
making adjustments for new db format
cschu Jan 7, 2025
21d4264
added count matrix state dump
cschu Jan 10, 2025
929e211
added count matrix state dump
cschu Jan 10, 2025
86f5f78
added count matrix state dump
cschu Jan 10, 2025
daac8f2
added count matrix state dump
cschu Jan 10, 2025
88fa6f5
added count matrix state dump
cschu Jan 10, 2025
12dbfb3
added count matrix state dump
cschu Jan 10, 2025
166aab4
added count matrix state dump
cschu Jan 10, 2025
a822d12
refactor group_gene_counts to be not in-place
cschu Jan 11, 2025
e3c86c1
refactor group_gene_counts to be not in-place
cschu Jan 11, 2025
c4a8fad
refactor group_gene_counts to be not in-place
cschu Jan 11, 2025
1f295bc
refactor group_gene_counts to be not in-place
cschu Jan 11, 2025
77cbf43
explicit int casts, ggroups -> tuple)
cschu Feb 5, 2025
791bb8b
fix merge conflicts
cschu Jul 19, 2026
9900242
fix merge conflicts
cschu Jul 19, 2026
581846f
fix
cschu Jul 20, 2026
a4268e5
fix
cschu Jul 21, 2026
7558752
commenting out various code blocks from merge
cschu Jul 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM ubuntu:22.04

LABEL maintainer="cschu1981@gmail.com"
LABEL version="2.18.5"
LABEL version="2.20.0"
LABEL description="gffquant - functional profiling of metagenomic/transcriptomic wgs samples"


Expand Down
1 change: 1 addition & 0 deletions gffquant/alignment/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from .aln_group import AlignmentGroup
from .pysam_alignment_processor import AlignmentProcessor
from .reference_hit import ReferenceHit
from .samflags import SamFlags
from .cigarops import CigarOps

Expand Down
3 changes: 3 additions & 0 deletions gffquant/alignment/aln_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ def get_all_hits(self, as_ambiguous=False):
except TypeError as err:
raise TypeError(f"Cannot derive sequencing library from tags: {aln.tags}") from err

# in region mode, there can be more hits
# (if the alignment overlaps multiple features of the target sequence)
# in gene mode, each alignment is a hit, i.e. there is at most 1 hit / alignment
yield aln.hits, n_aln

def get_ambig_align_counts(self):
Expand Down
38 changes: 38 additions & 0 deletions gffquant/alignment/reference_hit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# pylint: disable=R0902

""" module docstring """

from dataclasses import dataclass, asdict


@dataclass(slots=True)
class ReferenceHit:
rid: int = None
start: int = None
end: int = None
rev_strand: bool = None
cov_start: int = None
cov_end: int = None
has_annotation: bool = None
n_aln: int = None
is_ambiguous: bool = None
library_mod: int = None
mate_id: int = None

def __hash__(self):
return hash(tuple(asdict(self).values()))

def __eq__(self, other):
return all(
item[0][1] == item[1][1]
for item in zip(
sorted(asdict(self).items()),
sorted(asdict(other).items())
)
)

def __str__(self):
return "\t".join(map(str, asdict(self).values()))

def __repr__(self):
return str(self)
5 changes: 4 additions & 1 deletion gffquant/annotation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@

""" module docstring """

from .count_annotator import GeneCountAnnotator, RegionCountAnnotator
# from .count_annotator import GeneCountAnnotator, RegionCountAnnotator
from .count_annotator import CountAnnotator
from .count_writer import CountWriter
from .genecount_annotator import GeneCountAnnotator
from .regioncount_annotator import RegionCountAnnotator
24 changes: 13 additions & 11 deletions gffquant/annotation/count_annotator.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def calc_scaling_factor(raw, normed, default=0):
return (raw / normed) if normed else default

total_uniq, total_uniq_normed, total_ambi, total_ambi_normed = self.total_counts
# total_uniq, total_ambi, total_uniq_normed, total_ambi_normed = self.total_counts
logger.info(
"TOTAL COUNTS: uraw=%s unorm=%s araw=%s anorm=%s",
total_uniq, total_uniq_normed, total_ambi, total_ambi_normed
Expand All @@ -111,19 +112,20 @@ def calc_scaling_factor(raw, normed, default=0):
total_ambi, total_ambi_normed, default_scaling_factor
)

total_uniq, total_uniq_normed, total_ambi, total_ambi_normed = self.total_gene_counts
logger.info(
"TOTAL GENE COUNTS: uraw=%s unorm=%s araw=%s anorm=%s",
total_uniq, total_uniq_normed, total_ambi, total_ambi_normed
)
# total_uniq, total_uniq_normed, total_ambi, total_ambi_normed = self.total_gene_counts
# total_uniq, total_ambi, total_uniq_normed, total_ambi_normed = self.total_gene_counts
# logger.info(
# "TOTAL GENE COUNTS: uraw=%s unorm=%s araw=%s anorm=%s",
# total_uniq, total_uniq_normed, total_ambi, total_ambi_normed
# )

self.scaling_factors["total_gene_uniq"] = calc_scaling_factor(
total_uniq, total_uniq_normed, default_scaling_factor
)
# self.scaling_factors["total_gene_uniq"] = calc_scaling_factor(
# total_uniq, total_uniq_normed, default_scaling_factor
# )

self.scaling_factors["total_gene_ambi"] = calc_scaling_factor(
total_ambi, total_ambi_normed, default_scaling_factor
)
# self.scaling_factors["total_gene_ambi"] = calc_scaling_factor(
# total_ambi, total_ambi_normed, default_scaling_factor
# )

fc_items = self.feature_count_sums.items()
for category, (
Expand Down
147 changes: 87 additions & 60 deletions gffquant/annotation/count_writer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# pylint: disable=C0103,W1514,R0913,R0917
# pylint: disable=C0103,W1514,R0913,R0917,R0914

""" module docstring """

Expand All @@ -8,6 +8,9 @@

import numpy as np

from ..counters import AlignmentCounter
from ..counters.count_matrix import CountMatrix


logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -74,6 +77,7 @@ def compile_block(raw, lnorm, scaling_factors):

p, row = 0, []
rpkm_factor = 1e9 / self.filtered_readcount

# unique counts
row += compile_block(*counts[p:p + 2], (scaling_factor, rpkm_factor,))
p += 2
Expand Down Expand Up @@ -107,69 +111,92 @@ def compile_block(raw, lnorm, scaling_factors):
def write_row(header, data, stream=sys.stdout):
print(header, *(f"{c:.5f}" for c in data), flush=True, sep="\t", file=stream)

# pylint: disable=R0914
def write_feature_counts(self, db, featcounts, unannotated_reads=None, report_unseen=True):
for category_id, counts in sorted(featcounts.items()):
scaling_factor, ambig_scaling_factor = featcounts.scaling_factors[
category_id
]
category = db.query_category(category_id).name
if "scaled" in self.publish_reports:
logger.info(
"SCALING FACTORS %s %s %s",
category, scaling_factor, ambig_scaling_factor
def write_category(
self,
category_id,
category_name,
category_sum,
counts,
# feature_names,
features,
unannotated_reads=None,
report_unseen=True,
):
with gzip.open(f"{self.out_prefix}.{category_name}.txt.gz", "wt") as feat_out:
header = self.get_header()
print("feature", *header, sep="\t", file=feat_out)

if unannotated_reads is not None:
print("unannotated", unannotated_reads, sep="\t", file=feat_out)

if "total_readcount" in self.publish_reports:
CountWriter.write_row(
"total_reads",
np.zeros(len(header)) + self.total_readcount,
stream=feat_out,
)
with gzip.open(f"{self.out_prefix}.{category}.txt.gz", "wt") as feat_out:
header = self.get_header()
print("feature", *header, sep="\t", file=feat_out)

if unannotated_reads is not None:
print("unannotated", unannotated_reads, sep="\t", file=feat_out)

if "total_readcount" in self.publish_reports:
CountWriter.write_row(
"total_reads",
np.zeros(len(header)) + self.total_readcount,
stream=feat_out,
)

if "filtered_readcount" in self.publish_reports:
CountWriter.write_row(
"filtered_reads",
np.zeros(len(header)) + self.filtered_readcount,
stream=feat_out,
)
if "filtered_readcount" in self.publish_reports:
CountWriter.write_row(
"filtered_reads",
np.zeros(len(header)) + self.filtered_readcount,
stream=feat_out,
)

if "category" in self.publish_reports:
cat_counts = counts.get(f"cat:::{category_id}")
if cat_counts is not None:
cat_row = self.compile_output_row(
cat_counts,
scaling_factor=featcounts.scaling_factors["total_uniq"],
ambig_scaling_factor=featcounts.scaling_factors["total_ambi"],
)
CountWriter.write_row("category", cat_row, stream=feat_out)

for feature in db.get_features(category_id):
f_counts = counts.get(str(feature.id), np.zeros(len(header)))
if report_unseen or f_counts.sum():
out_row = self.compile_output_row(
f_counts,
scaling_factor=scaling_factor,
ambig_scaling_factor=ambig_scaling_factor,
)
CountWriter.write_row(feature.name, out_row, stream=feat_out)

def write_gene_counts(self, gene_counts, uniq_scaling_factor, ambig_scaling_factor):
if "scaled" in self.publish_reports:
logger.info("SCALING_FACTORS %s %s", uniq_scaling_factor, ambig_scaling_factor)
if "category" in self.publish_reports:
# cat_counts = counts[0]
cat_counts = category_sum
logger.info("CAT %s: %s", category_name, str(cat_counts))
if cat_counts is not None:
CountWriter.write_row("category", category_sum, stream=feat_out)

# for item in counts:
# if not isinstance(item[0], tuple):
# logger.info("ITEM: %s", str(item))
# raise TypeError(f"Weird key: {str(item)}")
# (cid, fid), fcounts = item
# if (report_unseen or fcounts.sum()) and cid == category_id:
# CountWriter.write_row(feature_names[fid], fcounts, stream=feat_out,)

empty_row = np.zeros(6, dtype=CountMatrix.NUMPY_DTYPE)
for feature in features:
key = (category_id, feature.id)
if counts.has_record(key):
row = counts[key]
else:
row = empty_row
if (report_unseen or row.sum()):
CountWriter.write_row(feature.name, row, stream=feat_out,)


# for (cid, fid), fcounts in counts:
# if (report_unseen or fcounts.sum()) and cid == category_id:
# CountWriter.write_row(feature_names[fid], fcounts, stream=feat_out,)

Comment on lines +114 to +175

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Multiple improvements needed.

  1. Remove commented-out code and parameters
  2. Add error handling for file operations
  3. Simplify the row assignment logic
  4. Add type hints and docstring
     def write_category(
         self,
-        category_id,
-        category_name,
-        category_sum,
-        counts,
-        # feature_names,
-        features,
-        unannotated_reads=None,
-        report_unseen=True,
+        category_id: int,
+        category_name: str,
+        category_sum: np.ndarray,
+        counts: CountMatrix,
+        features: List[Feature],
+        unannotated_reads: Optional[float] = None,
+        report_unseen: bool = True,
-    ):
+    ) -> None:
+        """Write category counts to a gzipped output file.
+
+        Args:
+            category_id: Category identifier
+            category_name: Category name
+            category_sum: Sum of counts for the category
+            counts: Matrix of counts
+            features: List of features
+            unannotated_reads: Count of unannotated reads
+            report_unseen: Whether to report features with zero counts
+
+        Raises:
+            IOError: If file operations fail
+        """
-        with gzip.open(f"{self.out_prefix}.{category_name}.txt.gz", "wt") as feat_out:
+        try:
+            with gzip.open(f"{self.out_prefix}.{category_name}.txt.gz", "wt") as feat_out:
                 # ... rest of the implementation ...
-                if counts.has_record(key):
-                    row = counts[key]
-                else:
-                    row = empty_row
+                row = counts[key] if counts.has_record(key) else empty_row
+        except IOError as e:
+            logger.error("Failed to write category %s: %s", category_name, str(e))
+            raise
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def write_category(
self,
category_id,
category_name,
category_sum,
counts,
# feature_names,
features,
unannotated_reads=None,
report_unseen=True,
):
with gzip.open(f"{self.out_prefix}.{category_name}.txt.gz", "wt") as feat_out:
header = self.get_header()
print("feature", *header, sep="\t", file=feat_out)
if unannotated_reads is not None:
print("unannotated", unannotated_reads, sep="\t", file=feat_out)
if "total_readcount" in self.publish_reports:
CountWriter.write_row(
"total_reads",
np.zeros(len(header)) + self.total_readcount,
stream=feat_out,
)
with gzip.open(f"{self.out_prefix}.{category}.txt.gz", "wt") as feat_out:
header = self.get_header()
print("feature", *header, sep="\t", file=feat_out)
if unannotated_reads is not None:
print("unannotated", unannotated_reads, sep="\t", file=feat_out)
if "total_readcount" in self.publish_reports:
CountWriter.write_row(
"total_reads",
np.zeros(len(header)) + self.total_readcount,
stream=feat_out,
)
if "filtered_readcount" in self.publish_reports:
CountWriter.write_row(
"filtered_reads",
np.zeros(len(header)) + self.filtered_readcount,
stream=feat_out,
)
if "filtered_readcount" in self.publish_reports:
CountWriter.write_row(
"filtered_reads",
np.zeros(len(header)) + self.filtered_readcount,
stream=feat_out,
)
if "category" in self.publish_reports:
cat_counts = counts.get(f"cat:::{category_id}")
if cat_counts is not None:
cat_row = self.compile_output_row(
cat_counts,
scaling_factor=featcounts.scaling_factors["total_uniq"],
ambig_scaling_factor=featcounts.scaling_factors["total_ambi"],
)
CountWriter.write_row("category", cat_row, stream=feat_out)
for feature in db.get_features(category_id):
f_counts = counts.get(str(feature.id), np.zeros(len(header)))
if report_unseen or f_counts.sum():
out_row = self.compile_output_row(
f_counts,
scaling_factor=scaling_factor,
ambig_scaling_factor=ambig_scaling_factor,
)
CountWriter.write_row(feature.name, out_row, stream=feat_out)
def write_gene_counts(self, gene_counts, uniq_scaling_factor, ambig_scaling_factor):
if "scaled" in self.publish_reports:
logger.info("SCALING_FACTORS %s %s", uniq_scaling_factor, ambig_scaling_factor)
if "category" in self.publish_reports:
# cat_counts = counts[0]
cat_counts = category_sum
logger.info("CAT %s: %s", category_name, str(cat_counts))
if cat_counts is not None:
CountWriter.write_row("category", category_sum, stream=feat_out)
# for item in counts:
# if not isinstance(item[0], tuple):
# logger.info("ITEM: %s", str(item))
# raise TypeError(f"Weird key: {str(item)}")
# (cid, fid), fcounts = item
# if (report_unseen or fcounts.sum()) and cid == category_id:
# CountWriter.write_row(feature_names[fid], fcounts, stream=feat_out,)
empty_row = np.zeros(6, dtype=CountMatrix.NUMPY_DTYPE)
for feature in features:
key = (category_id, feature.id)
if counts.has_record(key):
row = counts[key]
else:
row = empty_row
if (report_unseen or row.sum()):
CountWriter.write_row(feature.name, row, stream=feat_out,)
# for (cid, fid), fcounts in counts:
# if (report_unseen or fcounts.sum()) and cid == category_id:
# CountWriter.write_row(feature_names[fid], fcounts, stream=feat_out,)
def write_category(
self,
category_id: int,
category_name: str,
category_sum: np.ndarray,
counts: CountMatrix,
features: List[Feature],
unannotated_reads: Optional[float] = None,
report_unseen: bool = True,
) -> None:
"""Write category counts to a gzipped output file.
Args:
category_id: Category identifier
category_name: Category name
category_sum: Sum of counts for the category
counts: Matrix of counts
features: List of features
unannotated_reads: Count of unannotated reads
report_unseen: Whether to report features with zero counts
Raises:
IOError: If file operations fail
"""
try:
with gzip.open(f"{self.out_prefix}.{category_name}.txt.gz", "wt") as feat_out:
header = self.get_header()
print("feature", *header, sep="\t", file=feat_out)
if unannotated_reads is not None:
print("unannotated", unannotated_reads, sep="\t", file=feat_out)
if "total_readcount" in self.publish_reports:
CountWriter.write_row(
"total_reads",
np.zeros(len(header)) + self.total_readcount,
stream=feat_out,
)
if "filtered_readcount" in self.publish_reports:
CountWriter.write_row(
"filtered_reads",
np.zeros(len(header)) + self.filtered_readcount,
stream=feat_out,
)
if "category" in self.publish_reports:
cat_counts = category_sum
logger.info("CAT %s: %s", category_name, str(cat_counts))
if cat_counts is not None:
CountWriter.write_row("category", category_sum, stream=feat_out)
empty_row = np.zeros(6, dtype=CountMatrix.NUMPY_DTYPE)
for feature in features:
key = (category_id, feature.id)
row = counts[key] if counts.has_record(key) else empty_row
if (report_unseen or row.sum()):
CountWriter.write_row(feature.name, row, stream=feat_out,)
except IOError as e:
logger.error("Failed to write category %s: %s", category_name, str(e))
raise
🧰 Tools
🪛 Ruff (0.8.2)

164-167: Use ternary operator row = counts[key] if counts.has_record(key) else empty_row instead of if-else-block

Replace if-else-block with row = counts[key] if counts.has_record(key) else empty_row

(SIM108)

def write_gene_counts(
self,
gene_counts: AlignmentCounter,
refmgr,
gene_group_db=False,
):
with gzip.open(f"{self.out_prefix}.gene_counts.txt.gz", "wt") as gene_out:
print("gene", *self.get_header(), sep="\t", file=gene_out, flush=True)

for gene, g_counts in sorted(gene_counts.items()):
out_row = self.compile_output_row(
g_counts,
scaling_factor=uniq_scaling_factor,
ambig_scaling_factor=ambig_scaling_factor
ref_stream = (
(
refmgr.get(rid[0] if isinstance(rid, tuple) else rid)[0],
rid,
)
CountWriter.write_row(gene, out_row, stream=gene_out)
for rid, _ in gene_counts
)

for ref, rid in sorted(ref_stream):
counts = gene_counts[rid]
# if gene_group_db:
# ref_tokens = ref.split(".")
# gene_id, _ = ".".join(ref_tokens[:-1]), ref_tokens[-1]
# else:
# gene_id = ref
gene_id = ref

CountWriter.write_row(gene_id, counts, stream=gene_out,)
Comment on lines +176 to +202

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Multiple improvements needed.

  1. Remove commented-out code
  2. Add error handling for file operations and reference lookups
  3. Add type hints and docstring
     def write_gene_counts(
         self,
-        gene_counts: AlignmentCounter,
-        refmgr,
-        gene_group_db=False,
+        gene_counts: AlignmentCounter,
+        refmgr: 'ReferenceManager',
+        gene_group_db: bool = False,
-    ):
+    ) -> None:
+        """Write gene counts to a gzipped output file.
+
+        Args:
+            gene_counts: Counter containing gene alignment data
+            refmgr: Reference manager for gene lookups
+            gene_group_db: Whether to parse gene IDs as group.version format
+
+        Raises:
+            IOError: If file operations fail
+            ValueError: If reference lookup fails
+        """
+        try:
             with gzip.open(f"{self.out_prefix}.gene_counts.txt.gz", "wt") as gene_out:
                 print("gene", *self.get_header(), sep="\t", file=gene_out, flush=True)

                 ref_stream = (
                     (
-                        refmgr.get(rid[0] if isinstance(rid, tuple) else rid)[0],
+                        ref_result := refmgr.get(rid[0] if isinstance(rid, tuple) else rid),
                         rid,
                     )
                     for rid, _ in gene_counts
+                    if ref_result is not None
                 )

                 for ref, rid in sorted(ref_stream):
+                    if not ref:
+                        logger.error("Reference not found for rid: %s", rid)
+                        continue
                     counts = gene_counts[rid]
-                    # if gene_group_db:
-                    #     ref_tokens = ref.split(".")
-                    #     gene_id, _ = ".".join(ref_tokens[:-1]), ref_tokens[-1]
-                    # else:
-                    #     gene_id = ref
                     gene_id = ref

                     CountWriter.write_row(gene_id, counts, stream=gene_out,)
+        except IOError as e:
+            logger.error("Failed to write gene counts: %s", str(e))
+            raise
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def write_gene_counts(
self,
gene_counts: AlignmentCounter,
refmgr,
gene_group_db=False,
):
with gzip.open(f"{self.out_prefix}.gene_counts.txt.gz", "wt") as gene_out:
print("gene", *self.get_header(), sep="\t", file=gene_out, flush=True)
for gene, g_counts in sorted(gene_counts.items()):
out_row = self.compile_output_row(
g_counts,
scaling_factor=uniq_scaling_factor,
ambig_scaling_factor=ambig_scaling_factor
ref_stream = (
(
refmgr.get(rid[0] if isinstance(rid, tuple) else rid)[0],
rid,
)
CountWriter.write_row(gene, out_row, stream=gene_out)
for rid, _ in gene_counts
)
for ref, rid in sorted(ref_stream):
counts = gene_counts[rid]
# if gene_group_db:
# ref_tokens = ref.split(".")
# gene_id, _ = ".".join(ref_tokens[:-1]), ref_tokens[-1]
# else:
# gene_id = ref
gene_id = ref
CountWriter.write_row(gene_id, counts, stream=gene_out,)
def write_gene_counts(
self,
gene_counts: AlignmentCounter,
refmgr: 'ReferenceManager',
gene_group_db: bool = False,
) -> None:
"""Write gene counts to a gzipped output file.
Args:
gene_counts: Counter containing gene alignment data
refmgr: Reference manager for gene lookups
gene_group_db: Whether to parse gene IDs as group.version format
Raises:
IOError: If file operations fail
ValueError: If reference lookup fails
"""
try:
with gzip.open(f"{self.out_prefix}.gene_counts.txt.gz", "wt") as gene_out:
print("gene", *self.get_header(), sep="\t", file=gene_out, flush=True)
ref_stream = (
(
ref_result := refmgr.get(rid[0] if isinstance(rid, tuple) else rid),
rid,
)
for rid, _ in gene_counts
if ref_result is not None
)
for ref, rid in sorted(ref_stream):
if not ref:
logger.error("Reference not found for rid: %s", rid)
continue
counts = gene_counts[rid]
gene_id = ref
CountWriter.write_row(gene_id, counts, stream=gene_out,)
except IOError as e:
logger.error("Failed to write gene counts: %s", str(e))
raise

79 changes: 79 additions & 0 deletions gffquant/annotation/genecount_annotator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# pylint: disable=R0914

""" module docstring """
import logging

import numpy as np

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove unused imports

The following imports are not used in the code:

  • numpy module
  • CountWriter class from .count_writer
-import numpy as np
-from .count_writer import CountWriter

Also applies to: 7-7


from .count_annotator import CountAnnotator
from ..counters import AlignmentCounter
from ..counters.count_matrix import CountMatrix
from ..db.annotation_db import AnnotationDatabaseManager


logger = logging.getLogger(__name__)


class GeneCountAnnotator(CountAnnotator):
""" CountAnnotator subclass for gene-based counting. """

def __init__(self, strand_specific, report_scaling_factors=True):
""" __init__() """
CountAnnotator.__init__(self, strand_specific, report_scaling_factors=report_scaling_factors)

def annotate_gene_counts(
self,
refmgr,
db: AnnotationDatabaseManager,
counter: AlignmentCounter,
gene_group_db=False
):
categories = list(db.get_categories())
category_sums = np.zeros((len(categories), 6))
functional_counts = CountMatrix(6)

# for category in categories:
# features = ((feature.name, feature) for feature in db.get_features(category.id))
# for _, feature in sorted(features, key=lambda x: x[0]):
# _ = functional_counts[(category.id, feature.id)]

for rid, counts in counter:
# counts = counter[rid]
if gene_group_db:
ggroup_id = rid
region_annotation = None
if ggroup_id != "0":
region_annotation = db.query_sequence(int(ggroup_id, 16), grouped_db=True,)
else:
ref, _ = refmgr.get(rid[0] if isinstance(rid, tuple) else rid)
region_annotation = db.query_sequence(ref)

if region_annotation is not None:
_, _, region_annotation = region_annotation
for category_id, features in region_annotation:
category_id = int(category_id)
category_sums[category_id] += counts
for feature_id in features:
feature_id = int(feature_id)
functional_counts[(category_id, feature_id)] += counts

functional_counts.drop_unindexed()

for i, category in enumerate(categories):
u_sf, c_sf = (
CountMatrix.calculate_scaling_factor(*category_sums[i][0:2]),
CountMatrix.calculate_scaling_factor(*category_sums[i][3:5]),
)

rows = tuple(
key[0] == category.id
for key, _ in functional_counts
)

functional_counts.scale_column(1, u_sf, rows=rows)
functional_counts.scale_column(4, c_sf, rows=rows)

category_sums[i, 2] = category_sums[i, 1] * u_sf
category_sums[i, 5] = category_sums[i, 4] * c_sf

return functional_counts, category_sums
Loading
Loading