From 379e5658a68032a2dc405472822932560f72c770 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 19 Feb 2026 16:16:58 +0100 Subject: [PATCH 001/236] Use hgvs_position_model dataclass for HGVS position results --- mutalyzer_crossmapper/hgvs_position_model.py | 40 ++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 mutalyzer_crossmapper/hgvs_position_model.py diff --git a/mutalyzer_crossmapper/hgvs_position_model.py b/mutalyzer_crossmapper/hgvs_position_model.py new file mode 100644 index 0000000..59915b7 --- /dev/null +++ b/mutalyzer_crossmapper/hgvs_position_model.py @@ -0,0 +1,40 @@ +""" +HGVS Position Model - + a dataclass object to bridge HGVS position component and Crossmapper outputs. +""" +from dataclasses import dataclass +from typing import Optional + +@dataclass +class HGVSPositionModel: + """ + Represent the position component of an HGVS variant description. + This model captures details necessary to describe the '[position]' part in an HGVS + description of the form + [reference sequence]:[sequence type].[position][variant type][change] + """ + position: int + offset: Optional[int] = None + region: Optional[str] = None + position_in_codon: Optional[int] = None + + + def __post_init__(self): + # validate position + if self.position <= 0: + raise ValueError("Position must be a positive integer.") + + # validate region + region_values = {"u", "-", "", "*", "d"} + if self.region is not None and self.region not in region_values: + raise ValueError( + f"Invalid region value: {self.region}. Allowed values are: {region_values}" + ) + + # validate position_in_codon + codon_values = {1, 2, 3} + if self.position_in_codon is not None and self.position_in_codon not in codon_values: + raise ValueError( + f"Invalid position in codon value: {self.position_in_codon}. " + f"Allowed values are: {codon_values}" + ) From bbd38dabbeeebf960d2012ff8e4a7788f5f71928 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 19 Feb 2026 18:20:30 +0100 Subject: [PATCH 002/236] Add function to convert from tuple to HGVSPositionModel --- mutalyzer_crossmapper/hgvs_position_model.py | 54 +++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/mutalyzer_crossmapper/hgvs_position_model.py b/mutalyzer_crossmapper/hgvs_position_model.py index 59915b7..7992c63 100644 --- a/mutalyzer_crossmapper/hgvs_position_model.py +++ b/mutalyzer_crossmapper/hgvs_position_model.py @@ -3,7 +3,7 @@ a dataclass object to bridge HGVS position component and Crossmapper outputs. """ from dataclasses import dataclass -from typing import Optional +from typing import Optional, Tuple @dataclass class HGVSPositionModel: @@ -38,3 +38,55 @@ def __post_init__(self): f"Invalid position in codon value: {self.position_in_codon}. " f"Allowed values are: {codon_values}" ) + + + # Convert from tuple to HGVSPositionModel + + #TODO: check for inverted and degerate options, now only support non-inverted and non-degenerate cases + @classmethod + def to_hgvs_position_model(cls, raw_tuple:Tuple): + """Convert crossmapper tuple to an HGVSPositionModel instance.""" + if not raw_tuple: + raise ValueError("Input tuple position cannot be empty.") + + # Genomic + if len(raw_tuple) == 1: + return cls(position=raw_tuple[0]) + # Non-coding + if len(raw_tuple) == 3: + pass + + # Coding + #(c_pos, offset, in_cds, offset_to_exon_boundary) + if len(raw_tuple) == 4: + c_pos, offset, cds, dis_to_exon_boundary = raw_tuple + region = cls._determine_region(cds, dis_to_exon_boundary) + return cls(position=c_pos, offset=offset, region=region) + + # Protein ( + if len(raw_tuple) == 5: + p_pos, codon_pos, offset, cds, dis_to_exon_boundary = raw_tuple + if cds == 0: # in CDS + return cls( + position=p_pos, + region="", + position_in_codon=codon_pos + ) + else: + # TODO: shall we support HGVSPositionModel outside of CDS for protein? + pass + + + @staticmethod + def _determine_region(cds, dis_to_exon_boundary): + if dis_to_exon_boundary < 0: + return "u" + elif dis_to_exon_boundary > 0: + return "d" + else: # in translation range, check if in CDS or not + if cds < 0: # before CDS + return "-" + elif cds > 0: # after CDS + return "*" + else: + return "" \ No newline at end of file From 5122c556840b7d143bb98f7ff5819a189a5df0ef Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 20 Feb 2026 11:36:05 +0100 Subject: [PATCH 003/236] Refactor(locus):return dict position model and update test --- mutalyzer_crossmapper/locus.py | 26 +++++++++++++------------- tests/test_locus.py | 32 ++++++++++++++++---------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 14a9d20..a692f61 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -11,32 +11,32 @@ def __init__(self, location, inverted=False): self._end = self.boundary[1] - self.boundary[0] def to_position(self, coordinate): - """Convert a coordinate to a proper position. + """Convert a coordinate to a proper position model. :arg int coordinate: Coordinate. - :returns tuple: Position. + :returns dict: Position model with 'position' and 'offset' keys. """ if self._inverted: if coordinate > self.boundary[1]: - return 0, self.boundary[1] - coordinate + return {"position": 0, "offset": self.boundary[1] - coordinate} if coordinate < self.boundary[0]: - return self._end, self.boundary[0] - coordinate - return self.boundary[1] - coordinate, 0 + return {"position": self._end, "offset": self.boundary[0] - coordinate} + return {"position": self.boundary[1] - coordinate, "offset": 0} - if coordinate < self.boundary[0]: - return 0, coordinate - self.boundary[0] - if coordinate > self.boundary[1]: - return self._end, coordinate - self.boundary[1] - return coordinate - self.boundary[0], 0 + if coordinate < self.boundary[0]: # upstream of an exon, re + return {"position": 0, "offset": coordinate - self.boundary[0]} + if coordinate > self.boundary[1]: # downstream of an exon + return {"position": self._end, "offset": coordinate - self.boundary[1]} + return {"position": coordinate - self.boundary[0], "offset": 0} def to_coordinate(self, position): """Convert a position to a coordinate. - :arg int position: Position. + :arg dict position: Position model with 'position' and 'offset' keys. :returns int: Coordinate. """ if self._inverted: - return self.boundary[1] - position[0] - position[1] - return self.boundary[0] + position[0] + position[1] + return self.boundary[1] - position["position"] - position["offset"] + return self.boundary[0] + position["position"] + position["offset"] diff --git a/tests/test_locus.py b/tests/test_locus.py index a873416..b650c1a 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -7,37 +7,37 @@ def test_Locus(): """Forward orientent Lovus.""" locus = Locus((30, 35)) - invariant(locus.to_position, 29, locus.to_coordinate, (0, -1)) - invariant(locus.to_position, 30, locus.to_coordinate, (0, 0)) - invariant(locus.to_position, 31, locus.to_coordinate, (1, 0)) - invariant(locus.to_position, 33, locus.to_coordinate, (3, 0)) - invariant(locus.to_position, 34, locus.to_coordinate, (4, 0)) - invariant(locus.to_position, 35, locus.to_coordinate, (4, 1)) + invariant(locus.to_position, 29, locus.to_coordinate, {"position": 0, "offset": -1}) + invariant(locus.to_position, 30, locus.to_coordinate, {"position": 0, "offset": 0}) + invariant(locus.to_position, 31, locus.to_coordinate, {"position": 1, "offset": 0}) + invariant(locus.to_position, 33, locus.to_coordinate, {"position": 3, "offset": 0}) + invariant(locus.to_position, 34, locus.to_coordinate, {"position": 4, "offset": 0}) + invariant(locus.to_position, 35, locus.to_coordinate, {"position": 4, "offset": 1}) def test_Locus_inverted(): """Reverse orientent Lovus.""" locus = Locus((30, 35), True) - invariant(locus.to_position, 35, locus.to_coordinate, (0, -1)) - invariant(locus.to_position, 34, locus.to_coordinate, (0, 0)) - invariant(locus.to_position, 33, locus.to_coordinate, (1, 0)) - invariant(locus.to_position, 31, locus.to_coordinate, (3, 0)) - invariant(locus.to_position, 30, locus.to_coordinate, (4, 0)) - invariant(locus.to_position, 29, locus.to_coordinate, (4, 1)) + invariant(locus.to_position, 35, locus.to_coordinate, {"position": 0, "offset": -1}) + invariant(locus.to_position, 34, locus.to_coordinate, {"position": 0, "offset": 0}) + invariant(locus.to_position, 33, locus.to_coordinate, {"position": 1, "offset": 0}) + invariant(locus.to_position, 31, locus.to_coordinate, {"position": 3, "offset": 0}) + invariant(locus.to_position, 30, locus.to_coordinate, {"position": 4, "offset": 0}) + invariant(locus.to_position, 29, locus.to_coordinate, {"position": 4, "offset": 1}) def test_Locus_degenerate(): """Degenerate positions are silently corrected.""" locus = Locus((10, 20)) - degenerate_equal(locus.to_coordinate, 9, [(0, -1), (-1, 0)]) - degenerate_equal(locus.to_coordinate, 20, [(9, 1), (10, 0)]) + degenerate_equal(locus.to_coordinate, 9, [{"position": 0, "offset": -1}, {"position": -1, "offset": 0}]) + degenerate_equal(locus.to_coordinate, 20, [{"position": 9, "offset": 1}, {"position": 10, "offset": 0}]) def test_Locus_inverted_degenerate(): """Degenerate positions are silently corrected.""" locus = Locus((10, 20), True) - degenerate_equal(locus.to_coordinate, 20, [(0, -1), (-1, 0)]) - degenerate_equal(locus.to_coordinate, 9, [(9, 1), (10, 0)]) + degenerate_equal(locus.to_coordinate, 20, [{"position": 0, "offset": -1}, {"position": -1, "offset": 0}]) + degenerate_equal(locus.to_coordinate, 9, [{"position": 9, "offset": 1}, {"position": 10, "offset": 0}]) From 836d7d3669561bba12e75494daf31c730717f222 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 20 Feb 2026 12:52:19 +0100 Subject: [PATCH 004/236] Refactor(multi_locus):return dict position model and update test --- mutalyzer_crossmapper/multi_locus.py | 30 ++++---- tests/test_multi_locus.py | 110 ++++++++++++++++++--------- 2 files changed, 88 insertions(+), 52 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 9836549..d1f65b8 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -19,7 +19,7 @@ def _offsets(locations, orientation): class MultiLocus(object): """MultiLocus object.""" - def __init__(self, locations, inverted=False): + def __init__(self, locations:list, inverted=False): """ :arg list locations: List of locus locations. :arg bool inverted: Orientation. @@ -36,7 +36,7 @@ def _direction(self, index): return len(self._offsets) - index - 1 return index - def outside(self, coordinate): + def outside(self, coordinate:int): """Calculate the offset relative to this MultiLocus. :arg int coordinate: Coordinate. @@ -49,32 +49,34 @@ def outside(self, coordinate): return coordinate - self._loci[-1].boundary[1] return 0 - def to_position(self, coordinate): + def to_position(self, coordinate:int): """Convert a coordinate to a position. :arg int coordinate: Coordinate. - :returns tuple: Position. + :returns dict: Position model. """ index = nearest_location(self._locations, coordinate, self._inverted) outside = self._orientation * self.outside(coordinate) + region = "u" if outside < 0 else "d" if outside > 0 else "" location = self._loci[index].to_position(coordinate) - return ( - location[0] + self._offsets[self._direction(index)], - location[1], - outside) + return {"position": location["position"] + self._offsets[self._direction(index)], + "offset": location["offset"], + "region": region} - def to_coordinate(self, position): - """Convert a position to a coordinate. + def to_coordinate(self, position_model:dict): + """Convert a position model to a coordinate. - :arg int position: Position. + :arg dict position: Position. :returns int: Coordinate. """ + offset_val = position_model["offset"] index = min( len(self._offsets), - max(0, bisect_right(self._offsets, position[0]) - 1)) - + max(0, bisect_right(self._offsets, position_model["position"]) - 1) + ) return self._loci[self._direction(index)].to_coordinate( - (position[0] - self._offsets[index], position[1])) + {"position": position_model["position"] - self._offsets[index], "offset": offset_val} + ) \ No newline at end of file diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index 6ce0013..1e3812a 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -1,3 +1,5 @@ +"""Tests for MultiLocus flattening and coordinate conversions.""" + from mutalyzer_crossmapper import MultiLocus from mutalyzer_crossmapper.multi_locus import _offsets @@ -15,7 +17,11 @@ def test_offsets_inverted(): """Cummulative location lengths for inverted list of locations.""" assert _offsets(_locations, -1) == [0, 2, 4, 8, 13, 19] - + invariant( + multi_locus.to_position, + 4, + multi_locus.to_coordinate, + {"position": 0, "offset": -1, "region": "u"}, def test_offsets_adjacent(): """Cummulative location lengths for adjacent locations.""" assert _offsets([(1, 3), (3, 5)], 1) == [0, 2] @@ -31,30 +37,32 @@ def test_MultiLocus(): multi_locus = MultiLocus(_locations) # Boundary between upstream and the first locus. + invariant(multi_locus.to_position, 4, multi_locus.to_coordinate, {"position": 0, "offset": -1, "region": "u"}, + ) + invariant( - multi_locus.to_position, 4, multi_locus.to_coordinate, (0, -1, -1)) - invariant( - multi_locus.to_position, 5, multi_locus.to_coordinate, (0, 0, 0)) + multi_locus.to_position, 5, multi_locus.to_coordinate, {"position": 0, "offset": 0, "region": ""}, + ) # Internal locus. invariant( - multi_locus.to_position, 29, multi_locus.to_coordinate, (9, -1, 0)) + multi_locus.to_position, 29, multi_locus.to_coordinate, {"position": 9, "offset": -1, "region": ""}) invariant( - multi_locus.to_position, 30, multi_locus.to_coordinate, (9, 0, 0)) + multi_locus.to_position, 30, multi_locus.to_coordinate, {"position": 9, "offset": 0, "region": ""}) invariant( - multi_locus.to_position, 31, multi_locus.to_coordinate, (10, 0, 0)) + multi_locus.to_position, 31, multi_locus.to_coordinate, {"position": 10, "offset": 0, "region": ""}) invariant( - multi_locus.to_position, 33, multi_locus.to_coordinate, (12, 0, 0)) + multi_locus.to_position, 33, multi_locus.to_coordinate, {"position": 12, "offset": 0, "region": ""}) invariant( - multi_locus.to_position, 34, multi_locus.to_coordinate, (13, 0, 0)) + multi_locus.to_position, 34, multi_locus.to_coordinate, {"position": 13, "offset": 0, "region": ""}) invariant( - multi_locus.to_position, 35, multi_locus.to_coordinate, (13, 1, 0)) + multi_locus.to_position, 35, multi_locus.to_coordinate, {"position": 13, "offset": 1, "region": ""}) # Boundary between the last locus and downstream. invariant( - multi_locus.to_position, 71, multi_locus.to_coordinate, (21, 0, 0)) + multi_locus.to_position, 71, multi_locus.to_coordinate, {"position": 21, "offset": 0, "region": ""}) invariant( - multi_locus.to_position, 72, multi_locus.to_coordinate, (21, 1, 1)) + multi_locus.to_position, 72, multi_locus.to_coordinate, {"position": 21, "offset": 1, "region": "d"}) def test_MultiLocus_inverted(): @@ -63,29 +71,29 @@ def test_MultiLocus_inverted(): # Boundary between upstream and the first locus. invariant( - multi_locus.to_position, 72, multi_locus.to_coordinate, (0, -1, -1)) + multi_locus.to_position, 72, multi_locus.to_coordinate, {"position": 0, "offset": -1, "region": "u"}) invariant( - multi_locus.to_position, 71, multi_locus.to_coordinate, (0, 0, 0)) + multi_locus.to_position, 71, multi_locus.to_coordinate, {"position": 0, "offset": 0, "region": ""}) # Internal locus. invariant( - multi_locus.to_position, 35, multi_locus.to_coordinate, (8, -1, 0)) + multi_locus.to_position, 35, multi_locus.to_coordinate, {"position": 8, "offset": -1, "region": ""}) invariant( - multi_locus.to_position, 34, multi_locus.to_coordinate, (8, 0, 0)) + multi_locus.to_position, 34, multi_locus.to_coordinate, {"position": 8, "offset": 0, "region": ""}) invariant( - multi_locus.to_position, 33, multi_locus.to_coordinate, (9, 0, 0)) + multi_locus.to_position, 33, multi_locus.to_coordinate, {"position": 9, "offset": 0, "region": ""}) invariant( - multi_locus.to_position, 31, multi_locus.to_coordinate, (11, 0, 0)) + multi_locus.to_position, 31, multi_locus.to_coordinate, {"position": 11, "offset": 0, "region": ""}) invariant( - multi_locus.to_position, 30, multi_locus.to_coordinate, (12, 0, 0)) + multi_locus.to_position, 30, multi_locus.to_coordinate, {"position": 12, "offset": 0, "region": ""}) invariant( - multi_locus.to_position, 29, multi_locus.to_coordinate, (12, 1, 0)) + multi_locus.to_position, 29, multi_locus.to_coordinate, {"position": 12, "offset": 1, "region": ""}) # Boundary between the last locus and downstream. invariant( - multi_locus.to_position, 5, multi_locus.to_coordinate, (21, 0, 0)) + multi_locus.to_position, 5, multi_locus.to_coordinate, {"position": 21, "offset": 0, "region": ""}) invariant( - multi_locus.to_position, 4, multi_locus.to_coordinate, (21, 1, 1)) + multi_locus.to_position, 4, multi_locus.to_coordinate, {"position": 21, "offset": 1, "region": "d"}) def test_MultiLocus_adjacent_loci(): @@ -93,9 +101,9 @@ def test_MultiLocus_adjacent_loci(): multi_locus = MultiLocus([(1, 3), (3, 5)]) invariant( - multi_locus.to_position, 2, multi_locus.to_coordinate, (1, 0, 0)) + multi_locus.to_position, 2, multi_locus.to_coordinate, {"position": 1, "offset": 0, "region": ""}) invariant( - multi_locus.to_position, 3, multi_locus.to_coordinate, (2, 0, 0)) + multi_locus.to_position, 3, multi_locus.to_coordinate, {"position": 2, "offset": 0, "region": ""}) def test_MultiLocus_adjacent_loci_inverted(): @@ -103,9 +111,9 @@ def test_MultiLocus_adjacent_loci_inverted(): multi_locus = MultiLocus([(1, 3), (3, 5)], True) invariant( - multi_locus.to_position, 3, multi_locus.to_coordinate, (1, 0, 0)) + multi_locus.to_position, 3, multi_locus.to_coordinate, {"position": 1, "offset": 0, "region": ""}) invariant( - multi_locus.to_position, 2, multi_locus.to_coordinate, (2, 0, 0)) + multi_locus.to_position, 2, multi_locus.to_coordinate, {"position": 2, "offset": 0, "region": ""}) def test_MultiLocus_offsets_odd(): @@ -113,9 +121,9 @@ def test_MultiLocus_offsets_odd(): multi_locus = MultiLocus([(1, 3), (6, 8)]) invariant( - multi_locus.to_position, 4, multi_locus.to_coordinate, (1, 2, 0)) + multi_locus.to_position, 4, multi_locus.to_coordinate, {"position": 1, "offset": 2, "region": ""}) invariant( - multi_locus.to_position, 5, multi_locus.to_coordinate, (2, -1, 0)) + multi_locus.to_position, 5, multi_locus.to_coordinate, {"position": 2, "offset": -1, "region": ""}) def test_MultiLocus_offsets_odd_inverted(): @@ -123,9 +131,9 @@ def test_MultiLocus_offsets_odd_inverted(): multi_locus = MultiLocus([(1, 3), (6, 8)], True) invariant( - multi_locus.to_position, 4, multi_locus.to_coordinate, (1, 2, 0)) + multi_locus.to_position, 4, multi_locus.to_coordinate, {"position": 1, "offset": 2, "region": ""}) invariant( - multi_locus.to_position, 3, multi_locus.to_coordinate, (2, -1, 0)) + multi_locus.to_position, 3, multi_locus.to_coordinate, {"position": 2, "offset": -1, "region": ""}) def test_MultiLocus_offsets_even(): @@ -133,9 +141,9 @@ def test_MultiLocus_offsets_even(): multi_locus = MultiLocus([(1, 3), (7, 9)]) invariant( - multi_locus.to_position, 4, multi_locus.to_coordinate, (1, 2, 0)) + multi_locus.to_position, 4, multi_locus.to_coordinate, {"position": 1, "offset": 2, "region": ""}) invariant( - multi_locus.to_position, 5, multi_locus.to_coordinate, (2, -2, 0)) + multi_locus.to_position, 5, multi_locus.to_coordinate, {"position": 2, "offset": -2, "region": ""}) def test_MultiLocus_offsets_even_inverted(): @@ -143,9 +151,9 @@ def test_MultiLocus_offsets_even_inverted(): multi_locus = MultiLocus([(1, 3), (7, 9)], True) invariant( - multi_locus.to_position, 5, multi_locus.to_coordinate, (1, 2, 0)) + multi_locus.to_position, 5, multi_locus.to_coordinate, {"position": 1, "offset": 2, "region": ""}) invariant( - multi_locus.to_position, 4, multi_locus.to_coordinate, (2, -2, 0)) + multi_locus.to_position, 4, multi_locus.to_coordinate, {"position": 2, "offset": -2, "region": ""}) def test_MultiLocus_degenerate(): @@ -153,9 +161,22 @@ def test_MultiLocus_degenerate(): multi_locus = MultiLocus(_locations) degenerate_equal( - multi_locus.to_coordinate, 4, [(0, -1, -1), (-1, 0, -1)]) + multi_locus.to_coordinate, + 4, + [ + {"position": 0, "offset": -1, "region": "u"}, + {"position": -1, "offset": 0, "region": "u"}, + ], + ) + degenerate_equal( - multi_locus.to_coordinate, 72, [(21, 1, 1), (22, 0, 1)]) + multi_locus.to_coordinate, + 72, + [ + {"position": 21, "offset": 1, "region": "d"}, + {"position": 22, "offset": 0, "region": "d"}, + ], + ) def test_MultiLocus_inverted_degenerate(): @@ -163,6 +184,19 @@ def test_MultiLocus_inverted_degenerate(): multi_locus = MultiLocus(_locations, True) degenerate_equal( - multi_locus.to_coordinate, 72, [(0, -1, -1), (-1, 0, -1)]) + multi_locus.to_coordinate, + 72, + [ + {"position": 0, "offset": -1, "region": "u"}, + {"position": -1, "offset": 0, "region": "u"}, + ], + ) + degenerate_equal( - multi_locus.to_coordinate, 4, [(21, 1, 1), (22, 0, 1)]) + multi_locus.to_coordinate, + 4, + [ + {"position": 21, "offset": 1, "region": "d"}, + {"position": 22, "offset": 0, "region": "d"}, + ], + ) From 89cdba7fb14545df896b8f20f049cbd4ed931d2e Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 20 Feb 2026 12:56:20 +0100 Subject: [PATCH 005/236] Fix typo --- tests/test_multi_locus.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index 1e3812a..a0eea27 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -1,5 +1,3 @@ -"""Tests for MultiLocus flattening and coordinate conversions.""" - from mutalyzer_crossmapper import MultiLocus from mutalyzer_crossmapper.multi_locus import _offsets @@ -17,11 +15,6 @@ def test_offsets_inverted(): """Cummulative location lengths for inverted list of locations.""" assert _offsets(_locations, -1) == [0, 2, 4, 8, 13, 19] - invariant( - multi_locus.to_position, - 4, - multi_locus.to_coordinate, - {"position": 0, "offset": -1, "region": "u"}, def test_offsets_adjacent(): """Cummulative location lengths for adjacent locations.""" assert _offsets([(1, 3), (3, 5)], 1) == [0, 2] From 2d4e2eb292ea52b45277ba041609841481ab659a Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 20 Feb 2026 14:11:11 +0100 Subject: [PATCH 006/236] Refactor(crossmapper): convert Genomic to dict-based position model --- mutalyzer_crossmapper/crossmapper.py | 11 ++++++----- tests/test_crossmapper.py | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 537efd3..def779b 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -1,3 +1,4 @@ +from turtle import position from .multi_locus import MultiLocus @@ -8,18 +9,18 @@ def coordinate_to_genomic(self, coordinate): :arg int coordinate: Coordinate. - :returns int: Genomic position. + :returns dict: Genomic position. """ - return coordinate + 1 + return {"position": coordinate + 1} - def genomic_to_coordinate(self, position): + def genomic_to_coordinate(self, position_m): """Convert a genomic position (g./m./o.) to a coordinate. - :arg int position: Genomic position. + :arg int position: Genomic position model. :returns int: Coordinate. """ - return position - 1 + return position_m["position"] - 1 class NonCoding(Genomic): diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index dbe6b47..1d019fb 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -11,9 +11,9 @@ def test_Genomic(): crossmap = Genomic() invariant( - crossmap.coordinate_to_genomic, 0, crossmap.genomic_to_coordinate, 1) + crossmap.coordinate_to_genomic, 0, crossmap.genomic_to_coordinate, {"position": 1}) invariant( - crossmap.coordinate_to_genomic, 98, crossmap.genomic_to_coordinate, 99) + crossmap.coordinate_to_genomic, 98, crossmap.genomic_to_coordinate, {"position": 99}) def test_NonCoding(): From 923cd9fb406b3ee916b17a87ed2a11d6cbc85285 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 20 Feb 2026 17:44:20 +0100 Subject: [PATCH 007/236] Refactor(crossmapper): convert NonCoding to dict-based position model --- mutalyzer_crossmapper/crossmapper.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index def779b..07285b7 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -1,4 +1,3 @@ -from turtle import position from .multi_locus import MultiLocus @@ -39,23 +38,24 @@ def coordinate_to_noncoding(self, coordinate): :arg int coordinate: Coordinate. - :returns tuple: Noncoding position. + :returns dict: Noncoding position model. """ - pos = self._noncoding.to_position(coordinate) - - return pos[0] + 1, pos[1], pos[2] + pos_m = self._noncoding.to_position(coordinate) + if pos_m["region"] == "": + pos_m["position"] = pos_m["position"] + 1 + return pos_m - def noncoding_to_coordinate(self, position): + def noncoding_to_coordinate(self, position_m): """Convert a noncoding position (n./r.) to a coordinate. - :arg tuple position: Noncoding position. + :arg dict postion_m: Noncoding position model. :returns int: Coordinate. """ - if position[0] > 0: - return self._noncoding.to_coordinate( - (position[0] - 1, position[1])) - return self._noncoding.to_coordinate(position) + if position_m["region"] == "": + # if position_m["position"] > 0: + position_m["position"] = position_m["position"] - 1 + return self._noncoding.to_coordinate(position_m) class Coding(NonCoding): From 259481b05d519ebf4df106b2c8112c899ca4e231 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 20 Feb 2026 17:49:41 +0100 Subject: [PATCH 008/236] Refactor (locus, multi_locus): allow 'u', 'd' for UTR areas in hgvs model --- mutalyzer_crossmapper/locus.py | 17 +++++++++--- mutalyzer_crossmapper/multi_locus.py | 39 ++++++++++++++++++---------- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index a692f61..7e26235 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -30,13 +30,22 @@ def to_position(self, coordinate): return {"position": self._end, "offset": coordinate - self.boundary[1]} return {"position": coordinate - self.boundary[0], "offset": 0} - def to_coordinate(self, position): - """Convert a position to a coordinate. + def to_coordinate(self, position_m): + """Convert a position model to a coordinate. :arg dict position: Position model with 'position' and 'offset' keys. :returns int: Coordinate. """ if self._inverted: - return self.boundary[1] - position["position"] - position["offset"] - return self.boundary[0] + position["position"] + position["offset"] + if position_m["region"] == "u": + return self.boundary[1] + position_m["position"] + elif position_m["region"] == "d": + return self.boundary[0] - position_m["position"] + return self.boundary[1] - position_m["position"] - position_m["offset"] + if position_m["region"] == "u": + return self.boundary[0] - position_m["position"] + elif position_m["region"] == "d": + return self.boundary[1] + position_m["position"] + else: + return self.boundary[0] + position_m["position"] + position_m["offset"] diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index d1f65b8..a7f1ca4 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -60,23 +60,34 @@ def to_position(self, coordinate:int): outside = self._orientation * self.outside(coordinate) region = "u" if outside < 0 else "d" if outside > 0 else "" location = self._loci[index].to_position(coordinate) - - return {"position": location["position"] + self._offsets[self._direction(index)], - "offset": location["offset"], - "region": region} - - def to_coordinate(self, position_model:dict): + if not outside: + return { + "position": location["position"] + self._offsets[self._direction(index)], + "offset": location["offset"], + "region": region} + else: + return { + "position": abs(self._offsets[self._direction(index)] - self._offsets[self._direction(index)] + 1), + "offset": 0, + "region":region + } + + def to_coordinate(self, position_m:dict): """Convert a position model to a coordinate. :arg dict position: Position. :returns int: Coordinate. """ - offset_val = position_model["offset"] - index = min( - len(self._offsets), - max(0, bisect_right(self._offsets, position_model["position"]) - 1) - ) - return self._loci[self._direction(index)].to_coordinate( - {"position": position_model["position"] - self._offsets[index], "offset": offset_val} - ) \ No newline at end of file + if position_m["region"] == "": + index = min( + len(self._offsets), + max(0, bisect_right(self._offsets, position_m["position"]) - 1) + ) + position_m["position"] = position_m["position"] - self._offsets[index] + + elif position_m["region"] == "u": + index = 0 + else: # "d" + index = len(self._offsets) -1 + return self._loci[self._direction(index)].to_coordinate(position_m) From 55fd346994bb8731eb854d03898ae99c3ca72858 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 20 Feb 2026 17:50:30 +0100 Subject: [PATCH 009/236] Refactor test modules for Genomic and NonCoding --- tests/test_crossmapper.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 1d019fb..ef68a48 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -23,18 +23,18 @@ def test_NonCoding(): # Boundary between upstream and transcript. invariant( crossmap.coordinate_to_noncoding, 4, - crossmap.noncoding_to_coordinate, (1, -1, -1)) + crossmap.noncoding_to_coordinate, {"position": 1, "offset": 0, "region":"u"}) invariant( crossmap.coordinate_to_noncoding, 5, - crossmap.noncoding_to_coordinate, (1, 0, 0)) + crossmap.noncoding_to_coordinate, {"position": 1, "offset": 0, "region": ""}) # Boundary between downstream and transcript. invariant( crossmap.coordinate_to_noncoding, 71, - crossmap.noncoding_to_coordinate, (22, 0, 0)) + crossmap.noncoding_to_coordinate, {"position": 22, "offset": 0, "region": ""}) invariant( crossmap.coordinate_to_noncoding, 72, - crossmap.noncoding_to_coordinate, (22, 1, 1)) + crossmap.noncoding_to_coordinate, {"position": 1, "offset": 0, "region": "d"}) def test_NonCoding_inverted(): @@ -44,18 +44,18 @@ def test_NonCoding_inverted(): # Boundary between upstream and transcript. invariant( crossmap.coordinate_to_noncoding, 72, - crossmap.noncoding_to_coordinate, (1, -1, -1)) + crossmap.noncoding_to_coordinate, {"position": 1, "offset": 0, "region": "u"}) invariant( crossmap.coordinate_to_noncoding, 71, - crossmap.noncoding_to_coordinate, (1, 0, 0)) + crossmap.noncoding_to_coordinate, {"position": 1, "offset": 0, "region": ""}) # Boundary between downstream and transcript. invariant( crossmap.coordinate_to_noncoding, 5, - crossmap.noncoding_to_coordinate, (22, 0, 0)) + crossmap.noncoding_to_coordinate, {"position": 22, "offset": 0, "region": ""}) invariant( crossmap.coordinate_to_noncoding, 4, - crossmap.noncoding_to_coordinate, (22, 1, 1)) + crossmap.noncoding_to_coordinate, {"position": 1, "offset": 0, "region": "d"}) def test_NonCoding_degenerate(): @@ -65,12 +65,12 @@ def test_NonCoding_degenerate(): # Boundary between upstream and transcript. degenerate_equal( crossmap.noncoding_to_coordinate, 4, - [(1, -1, -1), (-1, 0, -1)]) + [{"position": 1, "offset": 0, "region":"u"}]) # Boundary between downstream and transcript. degenerate_equal( crossmap.noncoding_to_coordinate, 72, - [(22, 1, 1), (23, 0, 1)]) + [{"position": 1, "offset": 0, "region": "d"}]) def test_NonCoding_inverted_degenerate(): @@ -80,12 +80,12 @@ def test_NonCoding_inverted_degenerate(): # Boundary between upstream and transcript. degenerate_equal( crossmap.noncoding_to_coordinate, 72, - [(1, -1, -1), (-1, 0, -1)]) + [{"position": 1, "offset": 0, "region": "u"}]) # Boundary between downstream and transcript. degenerate_equal( crossmap.noncoding_to_coordinate, 4, - [(22, 1, 1), (23, 0, 1)]) + [{"position": 1 , "offset": 0, "region": "d"}]) def test_Coding(): From 84127e5aabeca1609c94febd90af7201957b3fb2 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 26 Feb 2026 09:53:56 +0100 Subject: [PATCH 010/236] Remove 'region' in locus position model --- mutalyzer_crossmapper/locus.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 7e26235..a531ecb 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -24,7 +24,7 @@ def to_position(self, coordinate): return {"position": self._end, "offset": self.boundary[0] - coordinate} return {"position": self.boundary[1] - coordinate, "offset": 0} - if coordinate < self.boundary[0]: # upstream of an exon, re + if coordinate < self.boundary[0]: # upstream of an exon return {"position": 0, "offset": coordinate - self.boundary[0]} if coordinate > self.boundary[1]: # downstream of an exon return {"position": self._end, "offset": coordinate - self.boundary[1]} @@ -38,14 +38,5 @@ def to_coordinate(self, position_m): :returns int: Coordinate. """ if self._inverted: - if position_m["region"] == "u": - return self.boundary[1] + position_m["position"] - elif position_m["region"] == "d": - return self.boundary[0] - position_m["position"] return self.boundary[1] - position_m["position"] - position_m["offset"] - if position_m["region"] == "u": - return self.boundary[0] - position_m["position"] - elif position_m["region"] == "d": - return self.boundary[1] + position_m["position"] - else: - return self.boundary[0] + position_m["position"] + position_m["offset"] + return self.boundary[0] + position_m["position"] + position_m["offset"] From eb6ce9b3d393e4089cbd368edfee720d2b150aaa Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 26 Feb 2026 10:01:09 +0100 Subject: [PATCH 011/236] Refactor(multi_locus): replace tuple with dict --- mutalyzer_crossmapper/multi_locus.py | 42 ++++++++++++++++++++-------- tests/test_multi_locus.py | 8 +++--- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index a7f1ca4..1e3f72a 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -60,16 +60,31 @@ def to_position(self, coordinate:int): outside = self._orientation * self.outside(coordinate) region = "u" if outside < 0 else "d" if outside > 0 else "" location = self._loci[index].to_position(coordinate) - if not outside: + # UTR + if outside: + return { + "position": abs(location["offset"]), + "offset": 0, + "region": region + } + # in exons + if location["offset"] == 0: # in an exon return { "position": location["position"] + self._offsets[self._direction(index)], - "offset": location["offset"], - "region": region} - else: - return { - "position": abs(self._offsets[self._direction(index)] - self._offsets[self._direction(index)] + 1), "offset": 0, - "region":region + "region": "" + } + elif location["offset"] < 0: # before an exon + return { + "position": self._offsets[self._direction(index)], + "offset": location["offset"], + "region": "" + } + else: # after an exon + return{ + "position": location["position"] + self._offsets[self._direction(index)], + "offset": location["offset"], + "region": "" } def to_coordinate(self, position_m:dict): @@ -85,9 +100,14 @@ def to_coordinate(self, position_m:dict): max(0, bisect_right(self._offsets, position_m["position"]) - 1) ) position_m["position"] = position_m["position"] - self._offsets[index] + return self._loci[self._direction(index)].to_coordinate(position_m) elif position_m["region"] == "u": - index = 0 - else: # "d" - index = len(self._offsets) -1 - return self._loci[self._direction(index)].to_coordinate(position_m) + if self._inverted: + return position_m["position"] + self._locations[-1][1] - 1 + return self._locations[0][0] - position_m["position"] + + else: # d + if self._inverted: + return self._locations[0][0] - position_m["position"] + return position_m["position"] + self._locations[-1][1] - 1 diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index a0eea27..dcd18ef 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -30,7 +30,7 @@ def test_MultiLocus(): multi_locus = MultiLocus(_locations) # Boundary between upstream and the first locus. - invariant(multi_locus.to_position, 4, multi_locus.to_coordinate, {"position": 0, "offset": -1, "region": "u"}, + invariant(multi_locus.to_position, 4, multi_locus.to_coordinate, {"position": 1, "offset": 0, "region": "u"}, ) invariant( @@ -55,7 +55,7 @@ def test_MultiLocus(): invariant( multi_locus.to_position, 71, multi_locus.to_coordinate, {"position": 21, "offset": 0, "region": ""}) invariant( - multi_locus.to_position, 72, multi_locus.to_coordinate, {"position": 21, "offset": 1, "region": "d"}) + multi_locus.to_position, 72, multi_locus.to_coordinate, {"position": 1, "offset": 0, "region": "d"}) def test_MultiLocus_inverted(): @@ -64,7 +64,7 @@ def test_MultiLocus_inverted(): # Boundary between upstream and the first locus. invariant( - multi_locus.to_position, 72, multi_locus.to_coordinate, {"position": 0, "offset": -1, "region": "u"}) + multi_locus.to_position, 72, multi_locus.to_coordinate, {"position": 1, "offset": 0, "region": "u"}) invariant( multi_locus.to_position, 71, multi_locus.to_coordinate, {"position": 0, "offset": 0, "region": ""}) @@ -86,7 +86,7 @@ def test_MultiLocus_inverted(): invariant( multi_locus.to_position, 5, multi_locus.to_coordinate, {"position": 21, "offset": 0, "region": ""}) invariant( - multi_locus.to_position, 4, multi_locus.to_coordinate, {"position": 21, "offset": 1, "region": "d"}) + multi_locus.to_position, 4, multi_locus.to_coordinate, {"position": 1, "offset": 0, "region": "d"}) def test_MultiLocus_adjacent_loci(): From 7d7bc9bd56dcb9ba04125b729a2e92738ed4aba7 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 26 Feb 2026 10:30:27 +0100 Subject: [PATCH 012/236] Refactor crossmapper and tests, allow ''/*/- in position model for Coding class --- mutalyzer_crossmapper/crossmapper.py | 86 ++++++++++++---- tests/test_crossmapper.py | 145 ++++++++++++++++++++------- 2 files changed, 173 insertions(+), 58 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 07285b7..8efa979 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -53,7 +53,6 @@ def noncoding_to_coordinate(self, position_m): :returns int: Coordinate. """ if position_m["region"] == "": - # if position_m["position"] > 0: position_m["position"] = position_m["position"] - 1 return self._noncoding.to_coordinate(position_m) @@ -72,11 +71,11 @@ def __init__(self, locations, cds, inverted=False): b1 = self._noncoding.to_position(cds[1]) if self._inverted: - self._coding = (b1[0] + b1[1] + 1, b0[0] + b0[1] + 1) - self._cds_len = (b0[0] + b0[1]) - (b1[0] + b1[1]) + self._coding = (b1["position"] + b1["offset"] + 1, b0["position"] + b0["offset"] + 1) + self._cds_len = (b0["position"] + b0["offset"]) - (b1["position"] + b1["offset"]) else: - self._coding = (b0[0] + b0[1], b1[0] + b1[1]) - self._cds_len = (b1[0] + b1[1]) - (b0[0] + b0[1]) + self._coding = (b0["position"] + b0["offset"], b1["position"] + b1["offset"]) + self._cds_len = (b1["position"] + b1["offset"]) - (b0["position"] + b0["offset"]) def _coordinate_to_coding(self, coordinate): """Convert a coordinate to a coding position (c./r.). @@ -85,13 +84,31 @@ def _coordinate_to_coding(self, coordinate): :returns tuple: Coding position (c./r.). """ - pos = self._noncoding.to_position(coordinate) - - if pos[0] < self._coding[0]: - return pos[0] - self._coding[0], pos[1], -1, pos[2] - elif pos[0] >= self._coding[1]: - return pos[0] - self._coding[1] + 1, pos[1], 1, pos[2] - return pos[0] - self._coding[0] + 1, pos[1], 0, pos[2] + noncoding_pos = self._noncoding.to_position(coordinate) + + # on top of the noncoding position model, add CDs info + location = noncoding_pos["position"] + if noncoding_pos["region"] == "": + if location < self._coding[0]: # before CDs + return { + "position": self._coding[0] - location, + "offset": noncoding_pos["offset"], + "region": "-" + } + elif location >= self._coding[1]: # after CDs + return { + "position": location - self._coding[1] + 1, + "offset": noncoding_pos["offset"], + "region": "*" + } + else: + return { + "position": location - self._coding[0] + 1, + "offset": noncoding_pos["offset"], + "region": "" + } + else: + return noncoding_pos def coordinate_to_coding(self, coordinate, degenerate=False): """Convert a coordinate to a coding position (c./r.). @@ -113,21 +130,46 @@ def coordinate_to_coding(self, coordinate, degenerate=False): return pos - def coding_to_coordinate(self, position): + def coding_to_coordinate(self, pos_m): """Convert a coding position (c./r.) to a coordinate. :arg tuple position: Coding position (c./r.). :returns int: Coordinate. """ - if position[2] == -1: - return self._noncoding.to_coordinate( - (position[0] + self._coding[0], position[1])) - elif position[2] == 1: - return self._noncoding.to_coordinate( - (position[0] + self._coding[1] - 1, position[1])) - return self._noncoding.to_coordinate( - (position[0] + self._coding[0] - 1, position[1])) + region = pos_m["region"] + if region == "u": + noncoding_pos = { + "position": pos_m["position"], + "offset": 0, + "region": "u" + } + elif region == "d": + noncoding_pos = { + "position": pos_m["position"], + "offset": 0, + "region": "d" + } + elif region == "": + noncoding_pos = { + "position": pos_m["position"] + self._coding[0] -1, + "offset": pos_m["offset"], + "region": "" + } + elif region == "-": + noncoding_pos = { + "position": self._coding[0] - pos_m["position"], + "offset": pos_m["offset"], + "region": "" + } + else: # * + noncoding_pos = { + "position": self._coding[1] + pos_m["position"] - 1, + "offset": pos_m["offset"], + "region": "" + } + return self._noncoding.to_coordinate(noncoding_pos) + def coordinate_to_protein(self, coordinate): """Convert a coordinate to a protein position (p.). @@ -138,7 +180,7 @@ def coordinate_to_protein(self, coordinate): """ pos = self.coordinate_to_coding(coordinate) - if pos[2] == -1: + if pos[2] == -1: # before CDs return (pos[0] // 3, pos[0] % 3 + 1, *pos[1:]) return ((pos[0] + 2) // 3, (pos[0] + 2) % 3 + 1, *pos[1:]) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index ef68a48..e2080ac 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -95,18 +95,37 @@ def test_Coding(): # Boundary between 5' and CDS. invariant( crossmap.coordinate_to_coding, 31, - crossmap.coding_to_coordinate, (-1, 0, -1, 0)) + crossmap.coding_to_coordinate, + {"position": 1, + "offset":0, + "region":"-" + } + ) invariant( crossmap.coordinate_to_coding, 32, - crossmap.coding_to_coordinate, (1, 0, 0, 0)) + crossmap.coding_to_coordinate, + {"position": 1, + "offset":0, + "region":"" + } + ) # Boundary between CDS and 3'. invariant( crossmap.coordinate_to_coding, 42, - crossmap.coding_to_coordinate, (6, 0, 0, 0)) + crossmap.coding_to_coordinate, + {"position": 6, + "offset":0, + "region":"" + }) invariant( crossmap.coordinate_to_coding, 43, - crossmap.coding_to_coordinate, (1, 0, 1, 0)) + crossmap.coding_to_coordinate, + {"position": 1, + "offset":0, + "region":"*" + } + ) def test_Coding_inverted(): @@ -116,18 +135,42 @@ def test_Coding_inverted(): # Boundary between 5' and CDS. invariant( crossmap.coordinate_to_coding, 43, - crossmap.coding_to_coordinate, (-1, 0, -1, 0)) + crossmap.coding_to_coordinate, + { + "position": 1, + "offset": 0, + "region": "-" + } + ) invariant( crossmap.coordinate_to_coding, 42, - crossmap.coding_to_coordinate, (1, 0, 0, 0)) + crossmap.coding_to_coordinate, + { + "position": 1, + "offset": 0, + "region": "" + } + ) # Boundary between CDS and 3'. invariant( crossmap.coordinate_to_coding, 32, - crossmap.coding_to_coordinate, (6, 0, 0, 0)) + crossmap.coding_to_coordinate, + { + "position": 6, + "offset": 0, + "region": "" + } + ) invariant( crossmap.coordinate_to_coding, 31, - crossmap.coding_to_coordinate, (1, 0, 1, 0)) + crossmap.coding_to_coordinate, + { + "position": 1, + "offset": 0, + "region": "*" + } + ) def test_Coding_regions(): @@ -137,18 +180,22 @@ def test_Coding_regions(): # Upstream odd length intron between two regions. invariant( crossmap.coordinate_to_coding, 25, - crossmap.coding_to_coordinate, (-1, 5, -1, 0)) + crossmap.coding_to_coordinate, + {'position': 1, 'offset': 5, 'region': '-'}) invariant( crossmap.coordinate_to_coding, 26, - crossmap.coding_to_coordinate, (1, -4, 0, 0)) + crossmap.coding_to_coordinate, + {'position': 1, 'offset': -4, 'region': ''}) # Downstream odd length intron between two regions. invariant( crossmap.coordinate_to_coding, 44, - crossmap.coding_to_coordinate, (10, 5, 0, 0)) + crossmap.coding_to_coordinate, + {'position': 10, 'offset': 5, 'region': ''}) invariant( crossmap.coordinate_to_coding, 45, - crossmap.coding_to_coordinate, (1, -4, 1, 0)) + crossmap.coding_to_coordinate, + {'position': 1, 'offset': -4, 'region': '*'}) def test_Coding_regions_inverted(): @@ -158,18 +205,22 @@ def test_Coding_regions_inverted(): # Upstream odd length intron between two regions. invariant( crossmap.coordinate_to_coding, 44, - crossmap.coding_to_coordinate, (-1, 5, -1, 0)) + crossmap.coding_to_coordinate, + {'position': 1, 'offset': 5, 'region': '-'}) invariant( crossmap.coordinate_to_coding, 43, - crossmap.coding_to_coordinate, (1, -4, 0, 0)) + crossmap.coding_to_coordinate, + {'position': 1, 'offset': -4, 'region': ''}) # Downstream odd length intron between two regions. invariant( crossmap.coordinate_to_coding, 25, - crossmap.coding_to_coordinate, (10, 5, 0, 0)) + crossmap.coding_to_coordinate, + {'position': 10, 'offset': 5, 'region': ''}) invariant( crossmap.coordinate_to_coding, 24, - crossmap.coding_to_coordinate, (1, -4, 1, 0)) + crossmap.coding_to_coordinate, + {'position': 1, 'offset': -4, 'region': '*'}) def test_Coding_no_utr5(): @@ -179,10 +230,12 @@ def test_Coding_no_utr5(): # Direct transition from upstream to CDS. invariant( crossmap.coordinate_to_coding, 9, - crossmap.coding_to_coordinate, (1, -1, 0, -1)) + crossmap.coding_to_coordinate, #(1, -1, 0, -1) + {'position': 1, 'offset': 0, 'region': 'u'}) invariant( crossmap.coordinate_to_coding, 10, - crossmap.coding_to_coordinate, (1, 0, 0, 0)) + crossmap.coding_to_coordinate, + {'position': 1, 'offset': 0, 'region': ''}) def test_Coding_no_utr5_inverted(): @@ -192,10 +245,12 @@ def test_Coding_no_utr5_inverted(): # Direct transition from upstream to CDS. invariant( crossmap.coordinate_to_coding, 20, - crossmap.coding_to_coordinate, (1, -1, 0, -1)) + crossmap.coding_to_coordinate, #(1, -1, 0, -1) + {'position': 1, 'offset': 0, 'region': 'u'}) invariant( crossmap.coordinate_to_coding, 19, - crossmap.coding_to_coordinate, (1, 0, 0, 0)) + crossmap.coding_to_coordinate, + {'position': 2, 'offset': 0, 'region': '-'}) def test_Coding_no_utr3(): @@ -203,12 +258,15 @@ def test_Coding_no_utr3(): crossmap = Coding([(10, 20)], (15, 20)) # Direct transition from CDS to downstream. + #TODO: invariant( crossmap.coordinate_to_coding, 19, - crossmap.coding_to_coordinate, (5, 0, 0, 0)) + crossmap.coding_to_coordinate, + {'position': 9, 'offset': 0, 'region': '*'}) invariant( crossmap.coordinate_to_coding, 20, - crossmap.coding_to_coordinate, (5, 1, 0, 1)) + crossmap.coding_to_coordinate, + {'position': 1, 'offset': 0, 'region': 'd'}) def test_Coding_no_utr3_inverted(): @@ -218,10 +276,12 @@ def test_Coding_no_utr3_inverted(): # Direct transition from CDS to downstream. invariant( crossmap.coordinate_to_coding, 10, - crossmap.coding_to_coordinate, (5, 0, 0, 0)) + crossmap.coding_to_coordinate, + {'position': 5, 'offset': 0, 'region': ''}) invariant( crossmap.coordinate_to_coding, 9, - crossmap.coding_to_coordinate, (5, 1, 0, 1)) + crossmap.coding_to_coordinate, + {'position': 1, 'offset': 0, 'region': 'd'}) def test_Coding_small_utr5(): @@ -231,13 +291,17 @@ def test_Coding_small_utr5(): # Transition from upstream to 5' UTR to CDS. invariant( crossmap.coordinate_to_coding, 9, - crossmap.coding_to_coordinate, (-1, -1, -1, -1)) + crossmap.coding_to_coordinate, #(-1, -1, -1, -1) + {'position': 1, 'offset': 0, 'region': 'u'}) invariant( crossmap.coordinate_to_coding, 10, - crossmap.coding_to_coordinate, (-1, 0, -1, 0)) + crossmap.coding_to_coordinate, #(-1, 0, -1, 0)) + {'position': 1, 'offset': 0, 'region': '-'} + ) invariant( crossmap.coordinate_to_coding, 11, - crossmap.coding_to_coordinate, (1, 0, 0, 0)) + crossmap.coding_to_coordinate, #(1, 0, 0, 0)) + {'position': 1, 'offset': 0, 'region': ''}) def test_Coding_small_utr5_inverted(): @@ -247,13 +311,16 @@ def test_Coding_small_utr5_inverted(): # Transition from upstream to 5' UTR to CDS. invariant( crossmap.coordinate_to_coding, 20, - crossmap.coding_to_coordinate, (-1, -1, -1, -1)) + crossmap.coding_to_coordinate,# (-1, -1, -1, -1) + {'position': 1, 'offset': 0, 'region': 'u'}) invariant( crossmap.coordinate_to_coding, 19, - crossmap.coding_to_coordinate, (-1, 0, -1, 0)) + crossmap.coding_to_coordinate, #(-1, 0, -1, 0)) + {'position': 1, 'offset': 0, 'region': '-'}) invariant( crossmap.coordinate_to_coding, 18, - crossmap.coding_to_coordinate, (1, 0, 0, 0)) + crossmap.coding_to_coordinate, + {'position': 1, 'offset': 0, 'region': ''}) def test_Coding_small_utr3(): @@ -263,13 +330,16 @@ def test_Coding_small_utr3(): # Transition from CDS to 3' UTR to downstream. invariant( crossmap.coordinate_to_coding, 18, - crossmap.coding_to_coordinate, (4, 0, 0, 0)) + crossmap.coding_to_coordinate, + {'position': 4, 'offset': 0, 'region': ''}) invariant( crossmap.coordinate_to_coding, 19, - crossmap.coding_to_coordinate, (1, 0, 1, 0)) + crossmap.coding_to_coordinate, #(1, 0, 1, 0) + {'position': 1, 'offset': 0, 'region': '*'}) invariant( crossmap.coordinate_to_coding, 20, - crossmap.coding_to_coordinate, (1, 1, 1, 1)) + crossmap.coding_to_coordinate, #(1, 1, 1, 1)) + {'position': 1, 'offset': 0, 'region': 'd'}) def test_Coding_small_utr3_inverted(): @@ -279,13 +349,16 @@ def test_Coding_small_utr3_inverted(): # Transition from CDS to 3' UTR to downstream. invariant( crossmap.coordinate_to_coding, 11, - crossmap.coding_to_coordinate, (4, 0, 0, 0)) + crossmap.coding_to_coordinate, + {'position': 4, 'offset': 0, 'region': ''}) invariant( crossmap.coordinate_to_coding, 10, - crossmap.coding_to_coordinate, (1, 0, 1, 0)) + crossmap.coding_to_coordinate, + {'position': 1, 'offset': 0, 'region': '*'}) invariant( crossmap.coordinate_to_coding, 9, - crossmap.coding_to_coordinate, (1, 1, 1, 1)) + crossmap.coding_to_coordinate, + {'position': 1, 'offset': 0, 'region': 'd'}) def test_Coding_degenerate(): From 043330389c052dc30b399142e2b17b7ec6c1df18 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 26 Feb 2026 18:29:26 +0100 Subject: [PATCH 013/236] Refactor(multi_locus): degenerate positions for multi_locus and tests --- mutalyzer_crossmapper/multi_locus.py | 13 +++++++------ tests/test_multi_locus.py | 13 ++++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 1e3f72a..d7a52e9 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -94,7 +94,8 @@ def to_coordinate(self, position_m:dict): :returns int: Coordinate. """ - if position_m["region"] == "": + region = position_m["region"] + if region == "": index = min( len(self._offsets), max(0, bisect_right(self._offsets, position_m["position"]) - 1) @@ -102,12 +103,12 @@ def to_coordinate(self, position_m:dict): position_m["position"] = position_m["position"] - self._offsets[index] return self._loci[self._direction(index)].to_coordinate(position_m) - elif position_m["region"] == "u": + elif region == "u": if self._inverted: - return position_m["position"] + self._locations[-1][1] - 1 - return self._locations[0][0] - position_m["position"] + return abs(position_m["position"]) + self._locations[-1][1] + position_m["offset"] - 1 + return self._locations[0][0] - abs(position_m["position"]) + position_m["offset"] else: # d if self._inverted: - return self._locations[0][0] - position_m["position"] - return position_m["position"] + self._locations[-1][1] - 1 + return self._locations[0][0] - abs(position_m["position"]) + position_m["offset"] + return abs(position_m["position"]) + self._locations[-1][1] + position_m["offset"] - 1 diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index dcd18ef..922529a 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -158,6 +158,7 @@ def test_MultiLocus_degenerate(): 4, [ {"position": 0, "offset": -1, "region": "u"}, + {"position": 1, "offset": 0, "region": "u"}, {"position": -1, "offset": 0, "region": "u"}, ], ) @@ -166,8 +167,8 @@ def test_MultiLocus_degenerate(): multi_locus.to_coordinate, 72, [ - {"position": 21, "offset": 1, "region": "d"}, - {"position": 22, "offset": 0, "region": "d"}, + {"position": 0, "offset": 1, "region": "d"}, + {"position": 1, "offset": 0, "region": "d"}, ], ) @@ -180,8 +181,9 @@ def test_MultiLocus_inverted_degenerate(): multi_locus.to_coordinate, 72, [ - {"position": 0, "offset": -1, "region": "u"}, + {"position": 0, "offset": 1, "region": "u"}, {"position": -1, "offset": 0, "region": "u"}, + {"position": 1, "offset": 0, "region": "u"}, ], ) @@ -189,7 +191,8 @@ def test_MultiLocus_inverted_degenerate(): multi_locus.to_coordinate, 4, [ - {"position": 21, "offset": 1, "region": "d"}, - {"position": 22, "offset": 0, "region": "d"}, + {"position": 0, "offset": -1, "region": "d"}, + {"position": 1, "offset": 0, "region": "d"}, + {"position": 2, "offset": 1, "region": "d"} ], ) From a583f00fcc038753243c046d9462169996626f7b Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 27 Feb 2026 11:44:31 +0100 Subject: [PATCH 014/236] Refactor(crossmapper): implement hgvs position model for protein and write degenerate in the same flow. --- mutalyzer_crossmapper/crossmapper.py | 58 ++++++++++++++++++++-------- tests/test_crossmapper.py | 36 +++++++++++------ 2 files changed, 67 insertions(+), 27 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 8efa979..af2b3f4 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -119,14 +119,30 @@ def coordinate_to_coding(self, coordinate, degenerate=False): :returns tuple: Coding position (c./r.). """ pos = self._coordinate_to_coding(coordinate) - - if degenerate and pos[3]: - if pos[2] == 0: - if pos[0] == 1 and pos[1] < 0: - return pos[1], 0, -1, pos[3] - if pos[0] == self._cds_len and pos[1] > 0: - return pos[0] + pos[1] - self._cds_len, 0, 1, pos[3] - return pos[0] + pos[1], 0, pos[2], pos[3] + # degenerate option: allow multiple or less correct ways to describe one position, + # e.g., neucleo c.10 can be the same location as c.d1 (if CDs ends at c9) + # the previous version corrects location+offset to location in UTR area (c.1-2->c.-2) + # or merge the offset to location + + if degenerate and pos["region"] in ["u", "d"]: + # if pos["region"] == "": unlikely to happen in biology? maybe used to collapse HGVS location at CDs boundary? + if pos["position"] == 1 and pos["offset"] < 0: + return { + "position":pos["offset"], + "offset": 0, + "region": "-" + } + if pos["position"] == self._cds_len and pos["offset"] > 0: + return { + "position": pos["position"] + pos["offset"] - self._cds_len, + "offset": 0, + "region": "*" + } + return { + "position": pos["position"] + pos["offset"], + "offset": 0, + "region":pos["region"] + } return pos @@ -140,13 +156,13 @@ def coding_to_coordinate(self, pos_m): region = pos_m["region"] if region == "u": noncoding_pos = { - "position": pos_m["position"], + "position": abs(pos_m["position"]) + pos_m["offset"], "offset": 0, "region": "u" } elif region == "d": noncoding_pos = { - "position": pos_m["position"], + "position": abs(pos_m["position"]) + pos_m["offset"], "offset": 0, "region": "d" } @@ -180,9 +196,15 @@ def coordinate_to_protein(self, coordinate): """ pos = self.coordinate_to_coding(coordinate) - if pos[2] == -1: # before CDs - return (pos[0] // 3, pos[0] % 3 + 1, *pos[1:]) - return ((pos[0] + 2) // 3, (pos[0] + 2) % 3 + 1, *pos[1:]) + if pos["region"] in ["-", "*"]: + return { + "position": pos["position"] // 3 + 1, + "position_in_codon": pos["position"] % 3, + **{k: v for k, v in pos.items() if k != "position"}} + return { + "position": (pos["position"]+2) // 3, + "position_in_codon": (pos["position"]+2) % 3 + 1, + **{k: v for k, v in pos.items() if k != "position"}} def protein_to_coordinate(self, position): """Convert a protein position (p.) to a coordinate. @@ -191,9 +213,13 @@ def protein_to_coordinate(self, position): :returns int: Coordinate. """ - if position[3] == -1: + if position["region"] in ["-", "*"]: return self.coding_to_coordinate( - (3 * position[0] + position[1] - 1, *position[2:])) + {"position": 3 * position["position"] + position["position_in_codon"] - 3, + "offset": position["offset"], + "region": position["region"]}) return self.coding_to_coordinate( - (3 * position[0] + position[1] - 3, *position[2:])) + {"position": 3 * position["position"] + position["position_in_codon"] - 3, + "offset": position["offset"], + "region": position["region"]}) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index e2080ac..e39225f 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -70,7 +70,10 @@ def test_NonCoding_degenerate(): # Boundary between downstream and transcript. degenerate_equal( crossmap.noncoding_to_coordinate, 72, - [{"position": 1, "offset": 0, "region": "d"}]) + [ + {"position": 1, "offset": 0, "region": "d"}, + {"position": 0, "offset": 1, "region": "d"} + ]) def test_NonCoding_inverted_degenerate(): @@ -87,7 +90,8 @@ def test_NonCoding_inverted_degenerate(): crossmap.noncoding_to_coordinate, 4, [{"position": 1 , "offset": 0, "region": "d"}]) - +_exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] +_cds = (32, 43) def test_Coding(): """Forward oriented coding transcript.""" crossmap = Coding(_exons, _cds) @@ -231,11 +235,11 @@ def test_Coding_no_utr5(): invariant( crossmap.coordinate_to_coding, 9, crossmap.coding_to_coordinate, #(1, -1, 0, -1) - {'position': 1, 'offset': 0, 'region': 'u'}) + {'position': 1, 'offset': 0, 'region': 'u'}) # serialize result : u1 invariant( crossmap.coordinate_to_coding, 10, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': ''}) + {'position': 1, 'offset': 0, 'region': ''}) # serialize result: 1 def test_Coding_no_utr5_inverted(): @@ -367,7 +371,11 @@ def test_Coding_degenerate(): degenerate_equal( crossmap.coding_to_coordinate, 9, - [(-1, -1, -1, -1), (-2, 0, -1, -1), (1, -2, 0, -1), (1, -10, 1, -1)]) + [ + {'position': 1, 'offset': 8, 'region': 'u'}, + {'position': 8, 'offset': 1, 'region': 'u'}, + {'position': -1, 'offset': 10, 'region': 'u'} + ]) degenerate_equal( crossmap.coding_to_coordinate, 20, [(1, 1, 1, 1), (2, 0, 1, 1), (8, 2, 0, 1), (-1, 10, -1, 1)]) @@ -466,23 +474,29 @@ def test_Coding_protein(): # Boundary between 5' UTR and CDS. invariant( crossmap.coordinate_to_protein, 31, - crossmap.protein_to_coordinate, (-1, 3, 0, -1, 0)) + crossmap.protein_to_coordinate, + {'position': 1, "position_in_codon": 1, 'offset': 0, 'region': '-'}) invariant( crossmap.coordinate_to_protein, 32, - crossmap.protein_to_coordinate, (1, 1, 0, 0, 0)) + crossmap.protein_to_coordinate, + {'position': 1, "position_in_codon": 1, 'offset': 0, 'region': ''}) # Intron boundary. invariant( crossmap.coordinate_to_protein, 34, - crossmap.protein_to_coordinate, (1, 3, 0, 0, 0)) + crossmap.protein_to_coordinate, + {'position': 1, "position_in_codon": 3, 'offset': 0, 'region': ''}) invariant( crossmap.coordinate_to_protein, 35, - crossmap.protein_to_coordinate, (1, 3, 1, 0, 0)) + crossmap.protein_to_coordinate, + {'position': 1, "position_in_codon": 3, 'offset': 1, 'region': ''}) # Boundary between CDS and 3' UTR. invariant( crossmap.coordinate_to_protein, 42, - crossmap.protein_to_coordinate, (2, 3, 0, 0, 0)) + crossmap.protein_to_coordinate, + {'position': 2, "position_in_codon": 3, 'offset': 0, 'region': ''}) invariant( crossmap.coordinate_to_protein, 43, - crossmap.protein_to_coordinate, (1, 1, 0, 1, 0)) + crossmap.protein_to_coordinate, + {'position': 1, "position_in_codon": 1, 'offset': 0, 'region': '*'}) From a57a3e6177ccb29125f3451f6b34a6b16c24d34c Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 5 Mar 2026 11:54:02 +0100 Subject: [PATCH 015/236] Refactor(crossmapper): discard degenerate option --- mutalyzer_crossmapper/crossmapper.py | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index af2b3f4..be0d1e1 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -110,7 +110,7 @@ def _coordinate_to_coding(self, coordinate): else: return noncoding_pos - def coordinate_to_coding(self, coordinate, degenerate=False): + def coordinate_to_coding(self, coordinate): """Convert a coordinate to a coding position (c./r.). :arg int coordinate: Coordinate. @@ -119,31 +119,6 @@ def coordinate_to_coding(self, coordinate, degenerate=False): :returns tuple: Coding position (c./r.). """ pos = self._coordinate_to_coding(coordinate) - # degenerate option: allow multiple or less correct ways to describe one position, - # e.g., neucleo c.10 can be the same location as c.d1 (if CDs ends at c9) - # the previous version corrects location+offset to location in UTR area (c.1-2->c.-2) - # or merge the offset to location - - if degenerate and pos["region"] in ["u", "d"]: - # if pos["region"] == "": unlikely to happen in biology? maybe used to collapse HGVS location at CDs boundary? - if pos["position"] == 1 and pos["offset"] < 0: - return { - "position":pos["offset"], - "offset": 0, - "region": "-" - } - if pos["position"] == self._cds_len and pos["offset"] > 0: - return { - "position": pos["position"] + pos["offset"] - self._cds_len, - "offset": 0, - "region": "*" - } - return { - "position": pos["position"] + pos["offset"], - "offset": 0, - "region":pos["region"] - } - return pos def coding_to_coordinate(self, pos_m): From d245cd6ed7749ba25838409ff2090e392ce2cdab Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 5 Mar 2026 14:37:40 +0100 Subject: [PATCH 016/236] Change examples in README. --- README.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 7862443..061f91b 100644 --- a/README.rst +++ b/README.rst @@ -53,8 +53,8 @@ positions and coordinates. >>> from mutalyzer_crossmapper import Genomic >>> crossmap = Genomic() >>> crossmap.coordinate_to_genomic(0) - 1 - >>> crossmap.genomic_to_coordinate(1) + {"position": 1} + >>> crossmap.genomic_to_coordinate({"position": 1}) 0 On top of the functionality provided by the ``Genomic`` class, the @@ -67,8 +67,8 @@ positions and coordinates. >>> exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] >>> crossmap = NonCoding(exons) >>> crossmap.coordinate_to_noncoding(35) - (14, 1, 0) - >>> crossmap.noncoding_to_coordinate((14, 1)) + {"position": 14, "offset": 1, "region": ""} + >>> crossmap.noncoding_to_coordinate({"position": 14, "offset": 1, "region": ""}) 35 Add the flag ``inverted=True`` to the constructor when the transcript resides @@ -84,8 +84,8 @@ coordinates as well as conversions between protein positions and coordinates. >>> cds = (32, 43) >>> crossmap = Coding(exons, cds) >>> crossmap.coordinate_to_coding(31) - (-1, 0, -1, 0) - >>> crossmap.coding_to_coordinate((-1, 0, -1)) + {"position": -1, "offset": 0, "region": "-"} + >>> crossmap.coding_to_coordinate({"position": -1, "offset": 0, "region": "-"}) 31 Again, the flag ``inverted=True`` can be used for transcripts that reside on @@ -96,8 +96,8 @@ Conversions between protein positions and coordinates are done as follows. .. code:: python >>> crossmap.coordinate_to_protein(41) - (2, 2, 0, 0, 0) - >>> crossmap.protein_to_coordinate((2, 2, 0, 0)) + {"position": 2, "position_in_codon": 2, "offset": 1, "region": ""} + >>> crossmap.protein_to_coordinate({"position": 2, "position_in_codon": 2, "offset": 1, "region": ""}) 41 From 89559e4f9e5c8ac142cc7683eada5616938bae58 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 6 Mar 2026 10:54:50 +0100 Subject: [PATCH 017/236] Discard dataclass object for hgvs position model --- mutalyzer_crossmapper/hgvs_position_model.py | 92 -------------------- 1 file changed, 92 deletions(-) delete mode 100644 mutalyzer_crossmapper/hgvs_position_model.py diff --git a/mutalyzer_crossmapper/hgvs_position_model.py b/mutalyzer_crossmapper/hgvs_position_model.py deleted file mode 100644 index 7992c63..0000000 --- a/mutalyzer_crossmapper/hgvs_position_model.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -HGVS Position Model - - a dataclass object to bridge HGVS position component and Crossmapper outputs. -""" -from dataclasses import dataclass -from typing import Optional, Tuple - -@dataclass -class HGVSPositionModel: - """ - Represent the position component of an HGVS variant description. - This model captures details necessary to describe the '[position]' part in an HGVS - description of the form - [reference sequence]:[sequence type].[position][variant type][change] - """ - position: int - offset: Optional[int] = None - region: Optional[str] = None - position_in_codon: Optional[int] = None - - - def __post_init__(self): - # validate position - if self.position <= 0: - raise ValueError("Position must be a positive integer.") - - # validate region - region_values = {"u", "-", "", "*", "d"} - if self.region is not None and self.region not in region_values: - raise ValueError( - f"Invalid region value: {self.region}. Allowed values are: {region_values}" - ) - - # validate position_in_codon - codon_values = {1, 2, 3} - if self.position_in_codon is not None and self.position_in_codon not in codon_values: - raise ValueError( - f"Invalid position in codon value: {self.position_in_codon}. " - f"Allowed values are: {codon_values}" - ) - - - # Convert from tuple to HGVSPositionModel - - #TODO: check for inverted and degerate options, now only support non-inverted and non-degenerate cases - @classmethod - def to_hgvs_position_model(cls, raw_tuple:Tuple): - """Convert crossmapper tuple to an HGVSPositionModel instance.""" - if not raw_tuple: - raise ValueError("Input tuple position cannot be empty.") - - # Genomic - if len(raw_tuple) == 1: - return cls(position=raw_tuple[0]) - # Non-coding - if len(raw_tuple) == 3: - pass - - # Coding - #(c_pos, offset, in_cds, offset_to_exon_boundary) - if len(raw_tuple) == 4: - c_pos, offset, cds, dis_to_exon_boundary = raw_tuple - region = cls._determine_region(cds, dis_to_exon_boundary) - return cls(position=c_pos, offset=offset, region=region) - - # Protein ( - if len(raw_tuple) == 5: - p_pos, codon_pos, offset, cds, dis_to_exon_boundary = raw_tuple - if cds == 0: # in CDS - return cls( - position=p_pos, - region="", - position_in_codon=codon_pos - ) - else: - # TODO: shall we support HGVSPositionModel outside of CDS for protein? - pass - - - @staticmethod - def _determine_region(cds, dis_to_exon_boundary): - if dis_to_exon_boundary < 0: - return "u" - elif dis_to_exon_boundary > 0: - return "d" - else: # in translation range, check if in CDS or not - if cds < 0: # before CDS - return "-" - elif cds > 0: # after CDS - return "*" - else: - return "" \ No newline at end of file From 08739ffbad75a76deb56ce4cc1930ad264e7cbf1 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 6 Mar 2026 16:21:40 +0100 Subject: [PATCH 018/236] Refactor(crossmapper): allow degenerate option for 'u' and 'd' area --- mutalyzer_crossmapper/crossmapper.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index be0d1e1..9056ade 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -110,7 +110,7 @@ def _coordinate_to_coding(self, coordinate): else: return noncoding_pos - def coordinate_to_coding(self, coordinate): + def coordinate_to_coding(self, coordinate, degenerate=False): """Convert a coordinate to a coding position (c./r.). :arg int coordinate: Coordinate. @@ -119,6 +119,13 @@ def coordinate_to_coding(self, coordinate): :returns tuple: Coding position (c./r.). """ pos = self._coordinate_to_coding(coordinate) + if degenerate and pos["region"] in ["u", "d"]: + if pos["region"] == "u": + pos["position"] = pos["position"] + self._coding[0] + pos["region"] = "-" + else: + pos["position"] = pos["position"] + self._coding[1] + pos["region"] = "*" return pos def coding_to_coordinate(self, pos_m): From fecf02617cdbb2a5d7c8c22ef216137d4abde455 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 9 Mar 2026 11:15:12 +0100 Subject: [PATCH 019/236] Cleanup(locus) --- mutalyzer_crossmapper/locus.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index a531ecb..738e87c 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -24,13 +24,13 @@ def to_position(self, coordinate): return {"position": self._end, "offset": self.boundary[0] - coordinate} return {"position": self.boundary[1] - coordinate, "offset": 0} - if coordinate < self.boundary[0]: # upstream of an exon + if coordinate < self.boundary[0]: return {"position": 0, "offset": coordinate - self.boundary[0]} - if coordinate > self.boundary[1]: # downstream of an exon + if coordinate > self.boundary[1]: return {"position": self._end, "offset": coordinate - self.boundary[1]} return {"position": coordinate - self.boundary[0], "offset": 0} - def to_coordinate(self, position_m): + def to_coordinate(self, pos_m): """Convert a position model to a coordinate. :arg dict position: Position model with 'position' and 'offset' keys. @@ -38,5 +38,5 @@ def to_coordinate(self, position_m): :returns int: Coordinate. """ if self._inverted: - return self.boundary[1] - position_m["position"] - position_m["offset"] - return self.boundary[0] + position_m["position"] + position_m["offset"] + return self.boundary[1] - pos_m["position"] - pos_m["offset"] + return self.boundary[0] + pos_m["position"] + pos_m["offset"] From 54746ec68c6304e324839987cc392a5554c97392 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 9 Mar 2026 11:37:55 +0100 Subject: [PATCH 020/236] Cleanup(multi_locus) --- mutalyzer_crossmapper/multi_locus.py | 37 +++++++++++++++------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index d7a52e9..b7b6ce4 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -54,61 +54,64 @@ def to_position(self, coordinate:int): :arg int coordinate: Coordinate. - :returns dict: Position model. + :returns dict: Position model 'position', 'offset' and 'region' keys. """ index = nearest_location(self._locations, coordinate, self._inverted) outside = self._orientation * self.outside(coordinate) region = "u" if outside < 0 else "d" if outside > 0 else "" location = self._loci[index].to_position(coordinate) - # UTR + if outside: return { "position": abs(location["offset"]), "offset": 0, "region": region } - # in exons - if location["offset"] == 0: # in an exon + + if location["offset"] == 0: return { "position": location["position"] + self._offsets[self._direction(index)], "offset": 0, "region": "" } - elif location["offset"] < 0: # before an exon + + elif location["offset"] < 0: return { "position": self._offsets[self._direction(index)], "offset": location["offset"], "region": "" } - else: # after an exon + + else: return{ "position": location["position"] + self._offsets[self._direction(index)], "offset": location["offset"], "region": "" } - def to_coordinate(self, position_m:dict): + def to_coordinate(self, pos_m:dict): """Convert a position model to a coordinate. - :arg dict position: Position. + :arg dict position: Position model with 'position','offset' and 'region' keys. :returns int: Coordinate. """ - region = position_m["region"] + region = pos_m["region"] + if region == "": index = min( len(self._offsets), - max(0, bisect_right(self._offsets, position_m["position"]) - 1) + max(0, bisect_right(self._offsets, pos_m["position"]) - 1) ) - position_m["position"] = position_m["position"] - self._offsets[index] - return self._loci[self._direction(index)].to_coordinate(position_m) + pos_m["position"] = pos_m["position"] - self._offsets[index] + return self._loci[self._direction(index)].to_coordinate(pos_m) elif region == "u": if self._inverted: - return abs(position_m["position"]) + self._locations[-1][1] + position_m["offset"] - 1 - return self._locations[0][0] - abs(position_m["position"]) + position_m["offset"] + return abs(pos_m["position"]) + self._locations[-1][1] + pos_m["offset"] - 1 + return self._locations[0][0] - abs(pos_m["position"]) + pos_m["offset"] - else: # d + else: if self._inverted: - return self._locations[0][0] - abs(position_m["position"]) + position_m["offset"] - return abs(position_m["position"]) + self._locations[-1][1] + position_m["offset"] - 1 + return self._locations[0][0] - abs(pos_m["position"]) + pos_m["offset"] + return abs(pos_m["position"]) + self._locations[-1][1] + pos_m["offset"] - 1 From 5ba6a5afdd7bd5395006dd70dc34122f1706aef3 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 9 Mar 2026 11:45:33 +0100 Subject: [PATCH 021/236] Cleanup(multi_locus) --- mutalyzer_crossmapper/multi_locus.py | 32 +++++++++++++--------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index b7b6ce4..6a71cc4 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -75,19 +75,18 @@ def to_position(self, coordinate:int): "region": "" } - elif location["offset"] < 0: + if location["offset"] < 0: return { "position": self._offsets[self._direction(index)], "offset": location["offset"], "region": "" } - else: - return{ - "position": location["position"] + self._offsets[self._direction(index)], - "offset": location["offset"], - "region": "" - } + return{ + "position": location["position"] + self._offsets[self._direction(index)], + "offset": location["offset"], + "region": "" + } def to_coordinate(self, pos_m:dict): """Convert a position model to a coordinate. @@ -98,20 +97,19 @@ def to_coordinate(self, pos_m:dict): """ region = pos_m["region"] - if region == "": - index = min( - len(self._offsets), - max(0, bisect_right(self._offsets, pos_m["position"]) - 1) - ) - pos_m["position"] = pos_m["position"] - self._offsets[index] - return self._loci[self._direction(index)].to_coordinate(pos_m) - - elif region == "u": + if region == "u": if self._inverted: return abs(pos_m["position"]) + self._locations[-1][1] + pos_m["offset"] - 1 return self._locations[0][0] - abs(pos_m["position"]) + pos_m["offset"] - else: + if region == "d": if self._inverted: return self._locations[0][0] - abs(pos_m["position"]) + pos_m["offset"] return abs(pos_m["position"]) + self._locations[-1][1] + pos_m["offset"] - 1 + + index = min( + len(self._offsets), + max(0, bisect_right(self._offsets, pos_m["position"]) - 1) + ) + pos_m["position"] = pos_m["position"] - self._offsets[index] + return self._loci[self._direction(index)].to_coordinate(pos_m) From 7f32afcc2cff00fdcbc7ba92d242548f1027b7f2 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 9 Mar 2026 11:50:43 +0100 Subject: [PATCH 022/236] Format(multi locus test) --- tests/test_multi_locus.py | 194 +++++++++++++++++++++++++++++++------- 1 file changed, 161 insertions(+), 33 deletions(-) diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index 922529a..c08ff7f 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -15,6 +15,7 @@ def test_offsets_inverted(): """Cummulative location lengths for inverted list of locations.""" assert _offsets(_locations, -1) == [0, 2, 4, 8, 13, 19] + def test_offsets_adjacent(): """Cummulative location lengths for adjacent locations.""" assert _offsets([(1, 3), (3, 5)], 1) == [0, 2] @@ -30,32 +31,71 @@ def test_MultiLocus(): multi_locus = MultiLocus(_locations) # Boundary between upstream and the first locus. - invariant(multi_locus.to_position, 4, multi_locus.to_coordinate, {"position": 1, "offset": 0, "region": "u"}, + invariant( + multi_locus.to_position, + 4, + multi_locus.to_coordinate, + {"position": 1, "offset": 0, "region": "u"}, ) invariant( - multi_locus.to_position, 5, multi_locus.to_coordinate, {"position": 0, "offset": 0, "region": ""}, + multi_locus.to_position, + 5, + multi_locus.to_coordinate, + {"position": 0, "offset": 0, "region": ""}, ) # Internal locus. invariant( - multi_locus.to_position, 29, multi_locus.to_coordinate, {"position": 9, "offset": -1, "region": ""}) + multi_locus.to_position, + 29, + multi_locus.to_coordinate, + {"position": 9, "offset": -1, "region": ""}, + ) invariant( - multi_locus.to_position, 30, multi_locus.to_coordinate, {"position": 9, "offset": 0, "region": ""}) + multi_locus.to_position, + 30, + multi_locus.to_coordinate, + {"position": 9, "offset": 0, "region": ""}, + ) invariant( - multi_locus.to_position, 31, multi_locus.to_coordinate, {"position": 10, "offset": 0, "region": ""}) + multi_locus.to_position, + 31, + multi_locus.to_coordinate, + {"position": 10, "offset": 0, "region": ""}, + ) invariant( - multi_locus.to_position, 33, multi_locus.to_coordinate, {"position": 12, "offset": 0, "region": ""}) + multi_locus.to_position, + 33, + multi_locus.to_coordinate, + {"position": 12, "offset": 0, "region": ""}, + ) invariant( - multi_locus.to_position, 34, multi_locus.to_coordinate, {"position": 13, "offset": 0, "region": ""}) + multi_locus.to_position, + 34, + multi_locus.to_coordinate, + {"position": 13, "offset": 0, "region": ""}, + ) invariant( - multi_locus.to_position, 35, multi_locus.to_coordinate, {"position": 13, "offset": 1, "region": ""}) + multi_locus.to_position, + 35, + multi_locus.to_coordinate, + {"position": 13, "offset": 1, "region": ""}, + ) # Boundary between the last locus and downstream. invariant( - multi_locus.to_position, 71, multi_locus.to_coordinate, {"position": 21, "offset": 0, "region": ""}) + multi_locus.to_position, + 71, + multi_locus.to_coordinate, + {"position": 21, "offset": 0, "region": ""}, + ) invariant( - multi_locus.to_position, 72, multi_locus.to_coordinate, {"position": 1, "offset": 0, "region": "d"}) + multi_locus.to_position, + 72, + multi_locus.to_coordinate, + {"position": 1, "offset": 0, "region": "d"}, + ) def test_MultiLocus_inverted(): @@ -64,29 +104,69 @@ def test_MultiLocus_inverted(): # Boundary between upstream and the first locus. invariant( - multi_locus.to_position, 72, multi_locus.to_coordinate, {"position": 1, "offset": 0, "region": "u"}) + multi_locus.to_position, + 72, + multi_locus.to_coordinate, + {"position": 1, "offset": 0, "region": "u"}, + ) invariant( - multi_locus.to_position, 71, multi_locus.to_coordinate, {"position": 0, "offset": 0, "region": ""}) + multi_locus.to_position, + 71, + multi_locus.to_coordinate, + {"position": 0, "offset": 0, "region": ""}, + ) # Internal locus. invariant( - multi_locus.to_position, 35, multi_locus.to_coordinate, {"position": 8, "offset": -1, "region": ""}) + multi_locus.to_position, + 35, + multi_locus.to_coordinate, + {"position": 8, "offset": -1, "region": ""}, + ) invariant( - multi_locus.to_position, 34, multi_locus.to_coordinate, {"position": 8, "offset": 0, "region": ""}) + multi_locus.to_position, + 34, + multi_locus.to_coordinate, + {"position": 8, "offset": 0, "region": ""}, + ) invariant( - multi_locus.to_position, 33, multi_locus.to_coordinate, {"position": 9, "offset": 0, "region": ""}) + multi_locus.to_position, + 33, + multi_locus.to_coordinate, + {"position": 9, "offset": 0, "region": ""}, + ) invariant( - multi_locus.to_position, 31, multi_locus.to_coordinate, {"position": 11, "offset": 0, "region": ""}) + multi_locus.to_position, + 31, + multi_locus.to_coordinate, + {"position": 11, "offset": 0, "region": ""}, + ) invariant( - multi_locus.to_position, 30, multi_locus.to_coordinate, {"position": 12, "offset": 0, "region": ""}) + multi_locus.to_position, + 30, + multi_locus.to_coordinate, + {"position": 12, "offset": 0, "region": ""}, + ) invariant( - multi_locus.to_position, 29, multi_locus.to_coordinate, {"position": 12, "offset": 1, "region": ""}) + multi_locus.to_position, + 29, + multi_locus.to_coordinate, + {"position": 12, "offset": 1, "region": ""}, + ) # Boundary between the last locus and downstream. invariant( - multi_locus.to_position, 5, multi_locus.to_coordinate, {"position": 21, "offset": 0, "region": ""}) + multi_locus.to_position, + 5, + multi_locus.to_coordinate, + {"position": 21, "offset": 0, "region": ""}, + ) invariant( - multi_locus.to_position, 4, multi_locus.to_coordinate, {"position": 1, "offset": 0, "region": "d"}) + multi_locus.to_position, + 4, + multi_locus.to_coordinate, + {"position": 1, "offset": 0, "region": "d"}, + ) def test_MultiLocus_adjacent_loci(): @@ -94,9 +174,17 @@ def test_MultiLocus_adjacent_loci(): multi_locus = MultiLocus([(1, 3), (3, 5)]) invariant( - multi_locus.to_position, 2, multi_locus.to_coordinate, {"position": 1, "offset": 0, "region": ""}) + multi_locus.to_position, + 2, + multi_locus.to_coordinate, + {"position": 1, "offset": 0, "region": ""}, + ) invariant( - multi_locus.to_position, 3, multi_locus.to_coordinate, {"position": 2, "offset": 0, "region": ""}) + multi_locus.to_position, + 3, + multi_locus.to_coordinate, + {"position": 2, "offset": 0, "region": ""}, + ) def test_MultiLocus_adjacent_loci_inverted(): @@ -104,9 +192,17 @@ def test_MultiLocus_adjacent_loci_inverted(): multi_locus = MultiLocus([(1, 3), (3, 5)], True) invariant( - multi_locus.to_position, 3, multi_locus.to_coordinate, {"position": 1, "offset": 0, "region": ""}) + multi_locus.to_position, + 3, + multi_locus.to_coordinate, + {"position": 1, "offset": 0, "region": ""}, + ) invariant( - multi_locus.to_position, 2, multi_locus.to_coordinate, {"position": 2, "offset": 0, "region": ""}) + multi_locus.to_position, + 2, + multi_locus.to_coordinate, + {"position": 2, "offset": 0, "region": ""}, + ) def test_MultiLocus_offsets_odd(): @@ -114,9 +210,17 @@ def test_MultiLocus_offsets_odd(): multi_locus = MultiLocus([(1, 3), (6, 8)]) invariant( - multi_locus.to_position, 4, multi_locus.to_coordinate, {"position": 1, "offset": 2, "region": ""}) + multi_locus.to_position, + 4, + multi_locus.to_coordinate, + {"position": 1, "offset": 2, "region": ""}, + ) invariant( - multi_locus.to_position, 5, multi_locus.to_coordinate, {"position": 2, "offset": -1, "region": ""}) + multi_locus.to_position, + 5, + multi_locus.to_coordinate, + {"position": 2, "offset": -1, "region": ""}, + ) def test_MultiLocus_offsets_odd_inverted(): @@ -124,9 +228,17 @@ def test_MultiLocus_offsets_odd_inverted(): multi_locus = MultiLocus([(1, 3), (6, 8)], True) invariant( - multi_locus.to_position, 4, multi_locus.to_coordinate, {"position": 1, "offset": 2, "region": ""}) + multi_locus.to_position, + 4, + multi_locus.to_coordinate, + {"position": 1, "offset": 2, "region": ""}, + ) invariant( - multi_locus.to_position, 3, multi_locus.to_coordinate, {"position": 2, "offset": -1, "region": ""}) + multi_locus.to_position, + 3, + multi_locus.to_coordinate, + {"position": 2, "offset": -1, "region": ""}, + ) def test_MultiLocus_offsets_even(): @@ -134,9 +246,17 @@ def test_MultiLocus_offsets_even(): multi_locus = MultiLocus([(1, 3), (7, 9)]) invariant( - multi_locus.to_position, 4, multi_locus.to_coordinate, {"position": 1, "offset": 2, "region": ""}) + multi_locus.to_position, + 4, + multi_locus.to_coordinate, + {"position": 1, "offset": 2, "region": ""}, + ) invariant( - multi_locus.to_position, 5, multi_locus.to_coordinate, {"position": 2, "offset": -2, "region": ""}) + multi_locus.to_position, + 5, + multi_locus.to_coordinate, + {"position": 2, "offset": -2, "region": ""}, + ) def test_MultiLocus_offsets_even_inverted(): @@ -144,9 +264,17 @@ def test_MultiLocus_offsets_even_inverted(): multi_locus = MultiLocus([(1, 3), (7, 9)], True) invariant( - multi_locus.to_position, 5, multi_locus.to_coordinate, {"position": 1, "offset": 2, "region": ""}) + multi_locus.to_position, + 5, + multi_locus.to_coordinate, + {"position": 1, "offset": 2, "region": ""}, + ) invariant( - multi_locus.to_position, 4, multi_locus.to_coordinate, {"position": 2, "offset": -2, "region": ""}) + multi_locus.to_position, + 4, + multi_locus.to_coordinate, + {"position": 2, "offset": -2, "region": ""}, + ) def test_MultiLocus_degenerate(): @@ -193,6 +321,6 @@ def test_MultiLocus_inverted_degenerate(): [ {"position": 0, "offset": -1, "region": "d"}, {"position": 1, "offset": 0, "region": "d"}, - {"position": 2, "offset": 1, "region": "d"} + {"position": 2, "offset": 1, "region": "d"}, ], ) From e23fe2abd02ebd8a2267f783cab225c2d7c10ebb Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 9 Mar 2026 13:41:45 +0100 Subject: [PATCH 023/236] Cleanup(crossmapper): on Genomic and NonCoding --- mutalyzer_crossmapper/crossmapper.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 9056ade..c90c15a 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -8,18 +8,18 @@ def coordinate_to_genomic(self, coordinate): :arg int coordinate: Coordinate. - :returns dict: Genomic position. + :returns dict: Genomic position model. """ return {"position": coordinate + 1} - def genomic_to_coordinate(self, position_m): + def genomic_to_coordinate(self, pos_m): """Convert a genomic position (g./m./o.) to a coordinate. - :arg int position: Genomic position model. + :arg dict position: Genomic position model. :returns int: Coordinate. """ - return position_m["position"] - 1 + return pos_m["position"] - 1 class NonCoding(Genomic): @@ -45,16 +45,16 @@ def coordinate_to_noncoding(self, coordinate): pos_m["position"] = pos_m["position"] + 1 return pos_m - def noncoding_to_coordinate(self, position_m): + def noncoding_to_coordinate(self, pos_m): """Convert a noncoding position (n./r.) to a coordinate. :arg dict postion_m: Noncoding position model. :returns int: Coordinate. """ - if position_m["region"] == "": - position_m["position"] = position_m["position"] - 1 - return self._noncoding.to_coordinate(position_m) + if pos_m["region"] == "": + pos_m["position"] = pos_m["position"] - 1 + return self._noncoding.to_coordinate(pos_m) class Coding(NonCoding): From 8f05341e9cde5b26c675bc4780be079ad5874038 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 9 Mar 2026 13:44:08 +0100 Subject: [PATCH 024/236] Cleanup --- mutalyzer_crossmapper/crossmapper.py | 4 ++-- mutalyzer_crossmapper/multi_locus.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index c90c15a..9762598 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -15,7 +15,7 @@ def coordinate_to_genomic(self, coordinate): def genomic_to_coordinate(self, pos_m): """Convert a genomic position (g./m./o.) to a coordinate. - :arg dict position: Genomic position model. + :arg dict pos_m: Genomic position model. :returns int: Coordinate. """ @@ -48,7 +48,7 @@ def coordinate_to_noncoding(self, coordinate): def noncoding_to_coordinate(self, pos_m): """Convert a noncoding position (n./r.) to a coordinate. - :arg dict postion_m: Noncoding position model. + :arg dict pos_m: Noncoding position model. :returns int: Coordinate. """ diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 6a71cc4..2853ee6 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -91,7 +91,7 @@ def to_position(self, coordinate:int): def to_coordinate(self, pos_m:dict): """Convert a position model to a coordinate. - :arg dict position: Position model with 'position','offset' and 'region' keys. + :arg dict pos_m: Position model with 'position','offset' and 'region' keys. :returns int: Coordinate. """ From 09f721ff0c74cedab27410ddf0cba198cdf8a581 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 09:56:23 +0100 Subject: [PATCH 025/236] Refactor degenerate tests --- tests/test_crossmapper.py | 114 +++++++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 26 deletions(-) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index e39225f..6ba2d83 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -65,7 +65,10 @@ def test_NonCoding_degenerate(): # Boundary between upstream and transcript. degenerate_equal( crossmap.noncoding_to_coordinate, 4, - [{"position": 1, "offset": 0, "region":"u"}]) + [ + {"position": 1, "offset": 0, "region":"u"}, + {"position": 0, "offset": -1, "region":"u"} + ]) # Boundary between downstream and transcript. degenerate_equal( @@ -234,7 +237,7 @@ def test_Coding_no_utr5(): # Direct transition from upstream to CDS. invariant( crossmap.coordinate_to_coding, 9, - crossmap.coding_to_coordinate, #(1, -1, 0, -1) + crossmap.coding_to_coordinate, {'position': 1, 'offset': 0, 'region': 'u'}) # serialize result : u1 invariant( crossmap.coordinate_to_coding, 10, @@ -249,7 +252,7 @@ def test_Coding_no_utr5_inverted(): # Direct transition from upstream to CDS. invariant( crossmap.coordinate_to_coding, 20, - crossmap.coding_to_coordinate, #(1, -1, 0, -1) + crossmap.coding_to_coordinate, {'position': 1, 'offset': 0, 'region': 'u'}) invariant( crossmap.coordinate_to_coding, 19, @@ -295,16 +298,16 @@ def test_Coding_small_utr5(): # Transition from upstream to 5' UTR to CDS. invariant( crossmap.coordinate_to_coding, 9, - crossmap.coding_to_coordinate, #(-1, -1, -1, -1) + crossmap.coding_to_coordinate, {'position': 1, 'offset': 0, 'region': 'u'}) invariant( crossmap.coordinate_to_coding, 10, - crossmap.coding_to_coordinate, #(-1, 0, -1, 0)) + crossmap.coding_to_coordinate, {'position': 1, 'offset': 0, 'region': '-'} ) invariant( crossmap.coordinate_to_coding, 11, - crossmap.coding_to_coordinate, #(1, 0, 0, 0)) + crossmap.coding_to_coordinate, {'position': 1, 'offset': 0, 'region': ''}) @@ -372,13 +375,29 @@ def test_Coding_degenerate(): degenerate_equal( crossmap.coding_to_coordinate, 9, [ - {'position': 1, 'offset': 8, 'region': 'u'}, - {'position': 8, 'offset': 1, 'region': 'u'}, - {'position': -1, 'offset': 10, 'region': 'u'} + {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 2, 'offset': 1, 'region': 'u'}, + {'position': 0, 'offset': -1, 'region': 'u'}, + {'position': 1, 'offset': -1, 'region': '-'}, + {'position': 2, 'offset': 0, 'region': '-'}, + {'position': 1, 'offset': -2, 'region': ''}, ]) degenerate_equal( crossmap.coding_to_coordinate, 20, - [(1, 1, 1, 1), (2, 0, 1, 1), (8, 2, 0, 1), (-1, 10, -1, 1)]) + [ + {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 8, 'offset': -7, 'region': 'd'}, + {'position': 0, 'offset': -1, 'region': 'd'}, + {'position': 2, 'offset': 0, 'region': '*'}, + {'position': 1, 'offset': 1, 'region': '*'}, + {'position': 8, 'offset': 2, 'region': ''}, + ] + ) + + +#TODO: Add tests for silently degenerate, +# position value <= 0 +# offset value > intron length def test_Coding_inverted_degenerate(): @@ -387,26 +406,46 @@ def test_Coding_inverted_degenerate(): degenerate_equal( crossmap.coding_to_coordinate, 20, - [(-1, -1, -1, -1), (-2, 0, -1, -1), (1, -2, 0, -1), (1, -10, 1, -1)]) + [ + {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 2, 'offset': 1, 'region': 'u'}, + {'position': 0, 'offset': -1, 'region': 'u'}, + {'position': 1, 'offset': -2, 'region': ''}, + {'position': 1, 'offset': -1, 'region': '-'}, + {'position': 2, 'offset': 0, 'region': '-'} + ] + ) degenerate_equal( crossmap.coding_to_coordinate, 9, - [(1, 1, 1, 1), (2, 0, 1, 1), (8, 2, 0, 1), (-1, 10, -1, 1)]) + [ + {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 2, 'offset': -1, 'region': 'd'}, + {'position': 1, 'offset': 1, 'region': '*'}, + {'position': 1, 'offset': 1, 'region': '*'}, + {'position': 10, 'offset': 0, 'region': ''}, + ] + ) def test_Coding_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" crossmap = Coding([(10, 20)], (11, 19)) - assert crossmap.coordinate_to_coding(9, True) == (-2, 0, -1, -1) - assert crossmap.coordinate_to_coding(20, True) == (2, 0, 1, 1) + for i in range(0, 30): + print(i, crossmap.coordinate_to_coding(i), crossmap.coordinate_to_coding(i, True)) + + assert crossmap.coordinate_to_coding(9, True) == {'position': 2, 'offset': 0, 'region': '-'} + assert crossmap.coordinate_to_coding(20, True) == {'position': 2, 'offset': 0, 'region': '*'} def test_Coding_inverted_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" crossmap = Coding([(10, 20)], (11, 19), True) + for i in range(0, 30): + print(i, crossmap.coordinate_to_coding(i), crossmap.coordinate_to_coding(i, True)) - assert crossmap.coordinate_to_coding(20, True) == (-2, 0, -1, -1) - assert crossmap.coordinate_to_coding(9, True) == (2, 0, 1, 1) + assert crossmap.coordinate_to_coding(20, True) == {'position': 2, 'offset': 0, 'region': '-'} + assert crossmap.coordinate_to_coding(9, True) == {'position': 2, 'offset': 0, 'region': '*'} def test_Coding_degenerate_no_return(): @@ -431,32 +470,55 @@ def test_Coding_no_utr_degenerate(): degenerate_equal( crossmap.coding_to_coordinate, 9, - [(1, -1, 0, -1), (-1, 0, -1, -1), (1, -2, 1, -1)]) + [ + {'position': 1, 'offset': 0, 'region': '-'}, + {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': -1, 'region': ''}, + ] + ) degenerate_equal( crossmap.coding_to_coordinate, 11, - [(1, 1, 0, 1), (1, 0, 1, 1), (-1, 2, -1, 1)]) - + [ + {'position': 1, 'offset': 0, 'region': '*'}, + {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 1, 'offset': 1, 'region': ''} + ] + ) def test_Coding_inverted_no_utr_degenerate(): """UTRs may be missing.""" crossmap = Coding([(10, 11)], (10, 11), True) + # [(1, -1, 0, -1), (-1, 0, -1, -1), (1, -2, 1, -1)]) degenerate_equal( crossmap.coding_to_coordinate, 11, - [(1, -1, 0, -1), (-1, 0, -1, -1), (1, -2, 1, -1)]) + [ + {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 2, 'offset': 1, 'region': 'u'}, + {'position': 1, 'offset': 0, 'region': '-'}, + {'position': 1, 'offset': 0, 'region': '*'}, + ] +) degenerate_equal( crossmap.coding_to_coordinate, 9, - [(1, 1, 0, 1), (1, 0, 1, 1), (-1, 2, -1, 1)]) - + [ + {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 1, 'offset': 0, 'region': '*'}, + {'position': 1, 'offset': -1, 'region': ''}, + ] + ) def test_Coding_no_utr_degenerate_return(): """UTRs may be missing.""" crossmap = Coding([(10, 11)], (10, 11)) - assert crossmap.coordinate_to_coding(8, True) == (-2, 0, -1, -2) - assert crossmap.coordinate_to_coding(9, True) == (-1, 0, -1, -1) - assert crossmap.coordinate_to_coding(11, True) == (1, 0, 1, 1) - assert crossmap.coordinate_to_coding(12, True) == (2, 0, 1, 2) + print(crossmap.coordinate_to_coding(11), crossmap.coordinate_to_coding(11, True)) + print(crossmap.coordinate_to_coding(12), crossmap.coordinate_to_coding(12, True)) + + assert crossmap.coordinate_to_coding(8, True) == {'position': 2, 'offset': 0, 'region': '-'}#(-2, 0, -1, -2) + assert crossmap.coordinate_to_coding(9, True) == {'position': 1, 'offset': 0, 'region': '-'}#(-1, 0, -1, -1) + assert crossmap.coordinate_to_coding(11, True) == {'position': 1, 'offset': 0, 'region': '*'}#(1, 0, 1, 1) + assert crossmap.coordinate_to_coding(12, True) == {'position': 2, 'offset': 0, 'region': '*'}#(2, 0, 1, 2) def test_Coding_inverted_no_utr_degenerate_return(): From abead481a3df79ef50a378788f2a3b136f05aae7 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 12:26:37 +0100 Subject: [PATCH 026/236] Add position model description for g.,n.,c.,and p. --- README.rst | 76 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 061f91b..03088d3 100644 --- a/README.rst +++ b/README.rst @@ -41,13 +41,22 @@ resides on the complement strand. Please see ReadTheDocs_ for the latest documentation. - Quick start ----------- +The `Genomic` class provides an interface to conversions between genomic positions and coordinates. -The ``Genomic`` class provides an interface to conversions between genomic -positions and coordinates. +***Genomic Position Model*** +Genomic positions follow the HGVS ``g`` coordinate system. They are represented +as dictionaries: + +.. code:: json + + {"position": int} +Where: +- ``position`` is a positive integer + +***Genomic Position Conversion*** .. code:: python >>> from mutalyzer_crossmapper import Genomic @@ -61,6 +70,23 @@ On top of the functionality provided by the ``Genomic`` class, the ``NonCoding`` class provides an interface to conversions between noncoding positions and coordinates. +***NonCoding Position Model*** +Noncoding positions follow the HGVS `n` coordinate system. They are represented +as dictionaries: +.. code:: json + { + "position": int, + "offset": int, + "region": str + } +Where: +- `position` is a positive interger +- `offset` is an interger indicating the offset relative to the position (e.g., +negative for upstream or positive for downstream) +- `region` uses string describing the region type (empty string `""` for standard +noncoding positions, `"u"` for upstream and `"d"` for downstream.) + +***NonCoding Position Conversion*** .. code:: python >>> from mutalyzer_crossmapper import NonCoding @@ -71,13 +97,32 @@ positions and coordinates. >>> crossmap.noncoding_to_coordinate({"position": 14, "offset": 1, "region": ""}) 35 -Add the flag ``inverted=True`` to the constructor when the transcript resides +****Notes**** +- Add the flag ``inverted=True`` to the constructor when the transcript resides on the reverse complement strand. On top of the functionality provided by the ``NonCoding`` class, the ``Coding`` class provides an interface to conversions between coding positions and coordinates as well as conversions between protein positions and coordinates. +***Coding Position Model*** +Coding positions follow the HGVS `c`` coordinate system. They are represented as +dictionaries: +.. code:: json + { + "position": int, + "offset": int, + "region": str + } +Where: +- `position` is a positive interger +- `offset` is an interger indicating the offset relative to the position (e.g., +negative for upstream or positive for downstream) +- `region` uses string describing the region type (empty string `""` for standard +coding positions, `"-"` for 5' UTR, `"*"` for 3' UTR, `"u"` for upstream and `"d"` +for downstream.) + +***Coding Position Conversion*** .. code:: python >>> from mutalyzer_crossmapper import Coding @@ -88,11 +133,30 @@ coordinates as well as conversions between protein positions and coordinates. >>> crossmap.coding_to_coordinate({"position": -1, "offset": 0, "region": "-"}) 31 -Again, the flag ``inverted=True`` can be used for transcripts that reside on +****Notes**** +- Again, the flag ``inverted=True`` can be used for transcripts that reside on the reverse complement strand. -Conversions between protein positions and coordinates are done as follows. +***Protein Position Model*** +Protein positions follow the HGVS `p`` coordinate system. They are represented +as dictionaries: +.. code:: json +{ + "position": int, + "position_in_codon": int, + "offset": int, + "region": str +} +Where: +- **position**: the amino acid position (1-based) +- **position_in_codon**: the codon nucleotide index (1, 2, or 3) +- **offset**: an integer indicating offset relative to the codon +- **region**: a string describing the region type (empty string `""`` for standard positions) + +***Protein Position Conversion*** + +Conversions between protein positions and coordinates are done as follows. .. code:: python >>> crossmap.coordinate_to_protein(41) From b528f926333ce8297aff3e21de15f5e3415d32f3 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 12:29:57 +0100 Subject: [PATCH 027/236] Format document --- README.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 03088d3..2aff9d5 100644 --- a/README.rst +++ b/README.rst @@ -45,7 +45,7 @@ Quick start ----------- The `Genomic` class provides an interface to conversions between genomic positions and coordinates. -***Genomic Position Model*** +**Genomic Position Model** Genomic positions follow the HGVS ``g`` coordinate system. They are represented as dictionaries: @@ -56,7 +56,7 @@ as dictionaries: Where: - ``position`` is a positive integer -***Genomic Position Conversion*** +**Genomic Position Conversion** .. code:: python >>> from mutalyzer_crossmapper import Genomic @@ -70,7 +70,7 @@ On top of the functionality provided by the ``Genomic`` class, the ``NonCoding`` class provides an interface to conversions between noncoding positions and coordinates. -***NonCoding Position Model*** +**NonCoding Position Model** Noncoding positions follow the HGVS `n` coordinate system. They are represented as dictionaries: .. code:: json @@ -86,7 +86,7 @@ negative for upstream or positive for downstream) - `region` uses string describing the region type (empty string `""` for standard noncoding positions, `"u"` for upstream and `"d"` for downstream.) -***NonCoding Position Conversion*** +**NonCoding Position Conversion** .. code:: python >>> from mutalyzer_crossmapper import NonCoding @@ -97,7 +97,7 @@ noncoding positions, `"u"` for upstream and `"d"` for downstream.) >>> crossmap.noncoding_to_coordinate({"position": 14, "offset": 1, "region": ""}) 35 -****Notes**** +***Notes*** - Add the flag ``inverted=True`` to the constructor when the transcript resides on the reverse complement strand. @@ -105,7 +105,7 @@ On top of the functionality provided by the ``NonCoding`` class, the ``Coding`` class provides an interface to conversions between coding positions and coordinates as well as conversions between protein positions and coordinates. -***Coding Position Model*** +**Coding Position Model** Coding positions follow the HGVS `c`` coordinate system. They are represented as dictionaries: .. code:: json @@ -122,7 +122,7 @@ negative for upstream or positive for downstream) coding positions, `"-"` for 5' UTR, `"*"` for 3' UTR, `"u"` for upstream and `"d"` for downstream.) -***Coding Position Conversion*** +**Coding Position Conversion** .. code:: python >>> from mutalyzer_crossmapper import Coding @@ -133,12 +133,12 @@ for downstream.) >>> crossmap.coding_to_coordinate({"position": -1, "offset": 0, "region": "-"}) 31 -****Notes**** +***Notes*** - Again, the flag ``inverted=True`` can be used for transcripts that reside on the reverse complement strand. -***Protein Position Model*** +**Protein Position Model** Protein positions follow the HGVS `p`` coordinate system. They are represented as dictionaries: .. code:: json @@ -154,7 +154,7 @@ Where: - **offset**: an integer indicating offset relative to the codon - **region**: a string describing the region type (empty string `""`` for standard positions) -***Protein Position Conversion*** +**Protein Position Conversion** Conversions between protein positions and coordinates are done as follows. .. code:: python From 6c1b52ed17a947f8565c4e9d48b9ad71663a4783 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 12:34:40 +0100 Subject: [PATCH 028/236] Format document --- README.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 2aff9d5..3d58a55 100644 --- a/README.rst +++ b/README.rst @@ -54,7 +54,7 @@ as dictionaries: {"position": int} Where: -- ``position`` is a positive integer +- **position**: a positive integer **Genomic Position Conversion** .. code:: python @@ -80,10 +80,10 @@ as dictionaries: "region": str } Where: -- `position` is a positive interger -- `offset` is an interger indicating the offset relative to the position (e.g., +- **position**: a positive interger +- **offset**: an interger indicating the offset relative to the position (e.g., negative for upstream or positive for downstream) -- `region` uses string describing the region type (empty string `""` for standard +- **region**: a string describing the region type (empty string `""` for standard noncoding positions, `"u"` for upstream and `"d"` for downstream.) **NonCoding Position Conversion** @@ -115,10 +115,10 @@ dictionaries: "region": str } Where: -- `position` is a positive interger -- `offset` is an interger indicating the offset relative to the position (e.g., +- **position**: a positive interger +- **offset**: an interger indicating the offset relative to the position (e.g., negative for upstream or positive for downstream) -- `region` uses string describing the region type (empty string `""` for standard +- **region**: a string describing the region type (empty string `""` for standard coding positions, `"-"` for 5' UTR, `"*"` for 3' UTR, `"u"` for upstream and `"d"` for downstream.) From b87160189983184859c90c8833811340d306baeb Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 12:40:34 +0100 Subject: [PATCH 029/236] Format document --- README.rst | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 3d58a55..cb9198a 100644 --- a/README.rst +++ b/README.rst @@ -54,6 +54,7 @@ as dictionaries: {"position": int} Where: + - **position**: a positive integer **Genomic Position Conversion** @@ -71,8 +72,7 @@ On top of the functionality provided by the ``Genomic`` class, the positions and coordinates. **NonCoding Position Model** -Noncoding positions follow the HGVS `n` coordinate system. They are represented -as dictionaries: +Noncoding positions follow the HGVS `n` coordinate system. They are represented as dictionaries: .. code:: json { "position": int, @@ -80,6 +80,7 @@ as dictionaries: "region": str } Where: + - **position**: a positive interger - **offset**: an interger indicating the offset relative to the position (e.g., negative for upstream or positive for downstream) @@ -97,7 +98,7 @@ noncoding positions, `"u"` for upstream and `"d"` for downstream.) >>> crossmap.noncoding_to_coordinate({"position": 14, "offset": 1, "region": ""}) 35 -***Notes*** +**Notes** - Add the flag ``inverted=True`` to the constructor when the transcript resides on the reverse complement strand. @@ -115,6 +116,7 @@ dictionaries: "region": str } Where: + - **position**: a positive interger - **offset**: an interger indicating the offset relative to the position (e.g., negative for upstream or positive for downstream) @@ -133,10 +135,8 @@ for downstream.) >>> crossmap.coding_to_coordinate({"position": -1, "offset": 0, "region": "-"}) 31 -***Notes*** -- Again, the flag ``inverted=True`` can be used for transcripts that reside on -the reverse complement strand. - +**Notes** +- Again, the flag ``inverted=True`` can be used for transcripts that reside on the reverse complement strand. **Protein Position Model** Protein positions follow the HGVS `p`` coordinate system. They are represented @@ -149,6 +149,7 @@ as dictionaries: "region": str } Where: + - **position**: the amino acid position (1-based) - **position_in_codon**: the codon nucleotide index (1, 2, or 3) - **offset**: an integer indicating offset relative to the codon From 9e91d1dd91b0168e8e51c7576db7f3036a9ffccb Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 13:50:43 +0100 Subject: [PATCH 030/236] Format document in .rst style --- README.rst | 167 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 96 insertions(+), 71 deletions(-) diff --git a/README.rst b/README.rst index cb9198a..cf51664 100644 --- a/README.rst +++ b/README.rst @@ -41,24 +41,30 @@ resides on the complement strand. Please see ReadTheDocs_ for the latest documentation. -Quick start ------------ -The `Genomic` class provides an interface to conversions between genomic positions and coordinates. +Quick Start +=========== -**Genomic Position Model** -Genomic positions follow the HGVS ``g`` coordinate system. They are represented -as dictionaries: +The ``Genomic`` class provides an interface for conversions between genomic positions and coordinates. -.. code:: json +Genomic Position Model +--------------------- - {"position": int} +Genomic positions follow the HGVS ``g`` coordinate system. They are represented as dictionaries: + +.. code-block:: json + + { + "position": int + } Where: - **position**: a positive integer -**Genomic Position Conversion** -.. code:: python +Genomic Position Conversion +-------------------------- + +.. code-block:: python >>> from mutalyzer_crossmapper import Genomic >>> crossmap = Genomic() @@ -67,28 +73,34 @@ Where: >>> crossmap.genomic_to_coordinate({"position": 1}) 0 -On top of the functionality provided by the ``Genomic`` class, the -``NonCoding`` class provides an interface to conversions between noncoding -positions and coordinates. - -**NonCoding Position Model** -Noncoding positions follow the HGVS `n` coordinate system. They are represented as dictionaries: -.. code:: json - { - "position": int, - "offset": int, - "region": str - } +NonCoding Class +--------------- + +The ``NonCoding`` class provides conversions between noncoding positions and coordinates. + +NonCoding Position Model +~~~~~~~~~~~~~~~~~~~~~~~ + +Noncoding positions follow the HGVS ``n`` coordinate system. They are represented as dictionaries: + +.. code-block:: json + + { + "position": int, + "offset": int, + "region": str + } + Where: -- **position**: a positive interger -- **offset**: an interger indicating the offset relative to the position (e.g., -negative for upstream or positive for downstream) -- **region**: a string describing the region type (empty string `""` for standard -noncoding positions, `"u"` for upstream and `"d"` for downstream.) +- **position**: a positive integer +- **offset**: an integer indicating the offset relative to the position (negative for upstream, positive for downstream) +- **region**: a string describing the region type (``""`` for standard, ``"u"`` for upstream, ``"d"`` for downstream) + +NonCoding Position Conversion +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**NonCoding Position Conversion** -.. code:: python +.. code-block:: python >>> from mutalyzer_crossmapper import NonCoding >>> exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] @@ -98,34 +110,39 @@ noncoding positions, `"u"` for upstream and `"d"` for downstream.) >>> crossmap.noncoding_to_coordinate({"position": 14, "offset": 1, "region": ""}) 35 -**Notes** -- Add the flag ``inverted=True`` to the constructor when the transcript resides -on the reverse complement strand. - -On top of the functionality provided by the ``NonCoding`` class, the ``Coding`` -class provides an interface to conversions between coding positions and -coordinates as well as conversions between protein positions and coordinates. - -**Coding Position Model** -Coding positions follow the HGVS `c`` coordinate system. They are represented as -dictionaries: -.. code:: json - { - "position": int, - "offset": int, - "region": str - } +Notes +~~~~~ + +- Add the flag ``inverted=True`` to the constructor when the transcript resides on the reverse complement strand. + +Coding Class +------------ + +The ``Coding`` class provides conversions between coding positions and coordinates, as well as protein positions. + +Coding Position Model +~~~~~~~~~~~~~~~~~~~~ + +Coding positions follow the HGVS ``c`` coordinate system. They are represented as dictionaries: + +.. code-block:: json + + { + "position": int, + "offset": int, + "region": str + } + Where: -- **position**: a positive interger -- **offset**: an interger indicating the offset relative to the position (e.g., -negative for upstream or positive for downstream) -- **region**: a string describing the region type (empty string `""` for standard -coding positions, `"-"` for 5' UTR, `"*"` for 3' UTR, `"u"` for upstream and `"d"` -for downstream.) +- **position**: a positive integer +- **offset**: an integer indicating the offset relative to the position +- **region**: a string describing the region type (``""`` for standard coding positions, ``"-"`` for 5' UTR, ``"*"`` for 3' UTR, ``"u"`` for upstream, ``"d"`` for downstream) + +Coding Position Conversion +~~~~~~~~~~~~~~~~~~~~~~~~~ -**Coding Position Conversion** -.. code:: python +.. code-block:: python >>> from mutalyzer_crossmapper import Coding >>> cds = (32, 43) @@ -135,30 +152,38 @@ for downstream.) >>> crossmap.coding_to_coordinate({"position": -1, "offset": 0, "region": "-"}) 31 -**Notes** -- Again, the flag ``inverted=True`` can be used for transcripts that reside on the reverse complement strand. - -**Protein Position Model** -Protein positions follow the HGVS `p`` coordinate system. They are represented -as dictionaries: -.. code:: json -{ - "position": int, - "position_in_codon": int, - "offset": int, - "region": str -} +Notes +~~~~~ + +- The flag ``inverted=True`` can be used for transcripts on the reverse complement strand. + +Protein Position Model +--------------------- + +Protein positions follow the HGVS ``p`` coordinate system. They are represented as dictionaries: + +.. code-block:: json + + { + "position": int, + "position_in_codon": int, + "offset": int, + "region": str + } + Where: - **position**: the amino acid position (1-based) - **position_in_codon**: the codon nucleotide index (1, 2, or 3) - **offset**: an integer indicating offset relative to the codon -- **region**: a string describing the region type (empty string `""`` for standard positions) +- **region**: a string describing the region type (``""`` for standard positions) + +Protein Position Conversion +~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Protein Position Conversion** +Conversions between protein positions and coordinates: -Conversions between protein positions and coordinates are done as follows. -.. code:: python +.. code-block:: python >>> crossmap.coordinate_to_protein(41) {"position": 2, "position_in_codon": 2, "offset": 1, "region": ""} From 35359b8916925356449fb8a084b0060db0e37eaa Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 14:05:21 +0100 Subject: [PATCH 031/236] Format document in .rst style --- README.rst | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index cf51664..630e9c1 100644 --- a/README.rst +++ b/README.rst @@ -44,10 +44,13 @@ Please see ReadTheDocs_ for the latest documentation. Quick Start =========== +Genomic Class +------------- + The ``Genomic`` class provides an interface for conversions between genomic positions and coordinates. Genomic Position Model ---------------------- +~~~~~~~~~~~~~~~~~~~~~~~ Genomic positions follow the HGVS ``g`` coordinate system. They are represented as dictionaries: @@ -58,11 +61,10 @@ Genomic positions follow the HGVS ``g`` coordinate system. They are represented } Where: - - **position**: a positive integer Genomic Position Conversion --------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python @@ -92,7 +94,6 @@ Noncoding positions follow the HGVS ``n`` coordinate system. They are represente } Where: - - **position**: a positive integer - **offset**: an integer indicating the offset relative to the position (negative for upstream, positive for downstream) - **region**: a string describing the region type (``""`` for standard, ``"u"`` for upstream, ``"d"`` for downstream) @@ -134,7 +135,6 @@ Coding positions follow the HGVS ``c`` coordinate system. They are represented a } Where: - - **position**: a positive integer - **offset**: an integer indicating the offset relative to the position - **region**: a string describing the region type (``""`` for standard coding positions, ``"-"`` for 5' UTR, ``"*"`` for 3' UTR, ``"u"`` for upstream, ``"d"`` for downstream) @@ -157,8 +157,11 @@ Notes - The flag ``inverted=True`` can be used for transcripts on the reverse complement strand. +Protein +------- + Protein Position Model ---------------------- +~~~~~~~~~~~~~~~~~~~~~~ Protein positions follow the HGVS ``p`` coordinate system. They are represented as dictionaries: @@ -179,7 +182,7 @@ Where: - **region**: a string describing the region type (``""`` for standard positions) Protein Position Conversion -~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Conversions between protein positions and coordinates: From d48f99d4a1a152d535ffb3b4ff2330815c06e4db Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 14:14:21 +0100 Subject: [PATCH 032/236] Format and add an example --- README.rst | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 630e9c1..73cd6a2 100644 --- a/README.rst +++ b/README.rst @@ -44,6 +44,14 @@ Please see ReadTheDocs_ for the latest documentation. Quick Start =========== +An example below uses the following transcript data: + +.. code-block:: python + + >>>_exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] + >>>_cds = (32, 43) + + Genomic Class ------------- @@ -61,6 +69,7 @@ Genomic positions follow the HGVS ``g`` coordinate system. They are represented } Where: + - **position**: a positive integer Genomic Position Conversion @@ -94,6 +103,7 @@ Noncoding positions follow the HGVS ``n`` coordinate system. They are represente } Where: + - **position**: a positive integer - **offset**: an integer indicating the offset relative to the position (negative for upstream, positive for downstream) - **region**: a string describing the region type (``""`` for standard, ``"u"`` for upstream, ``"d"`` for downstream) @@ -104,8 +114,7 @@ NonCoding Position Conversion .. code-block:: python >>> from mutalyzer_crossmapper import NonCoding - >>> exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] - >>> crossmap = NonCoding(exons) + >>> crossmap = NonCoding(_exons) >>> crossmap.coordinate_to_noncoding(35) {"position": 14, "offset": 1, "region": ""} >>> crossmap.noncoding_to_coordinate({"position": 14, "offset": 1, "region": ""}) @@ -135,9 +144,10 @@ Coding positions follow the HGVS ``c`` coordinate system. They are represented a } Where: + - **position**: a positive integer - **offset**: an integer indicating the offset relative to the position -- **region**: a string describing the region type (``""`` for standard coding positions, ``"-"`` for 5' UTR, ``"*"`` for 3' UTR, ``"u"`` for upstream, ``"d"`` for downstream) +- **region**: a string describing the region type (``""`` for standard coding positions, ``"-"`` for 5' UTR, ``"*"`` for 3' UTR, ``"u"`` for upstream and ``"d"`` for downstream) Coding Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -145,8 +155,7 @@ Coding Position Conversion .. code-block:: python >>> from mutalyzer_crossmapper import Coding - >>> cds = (32, 43) - >>> crossmap = Coding(exons, cds) + >>> crossmap = Coding(_exons, _cds) >>> crossmap.coordinate_to_coding(31) {"position": -1, "offset": 0, "region": "-"} >>> crossmap.coding_to_coordinate({"position": -1, "offset": 0, "region": "-"}) From d16278bf1938fb965c331faae541aabf0a94c406 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 14:23:39 +0100 Subject: [PATCH 033/236] Add table for Genomic positions and coordinate mapping --- README.rst | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/README.rst b/README.rst index 73cd6a2..6a3f600 100644 --- a/README.rst +++ b/README.rst @@ -84,6 +84,51 @@ Genomic Position Conversion >>> crossmap.genomic_to_coordinate({"position": 1}) 0 +Here is the mapping of coordinates to genomic positions: ++------------+----------+ +| Coordinate | Position | ++============+==========+ +| 0 | 1 | ++------------+----------+ +| 1 | 2 | ++------------+----------+ +| 2 | 3 | ++------------+----------+ +| 3 | 4 | ++------------+----------+ +| 4 | 5 | ++------------+----------+ +| 5 | 6 | ++------------+----------+ +| 6 | 7 | ++------------+----------+ +| 7 | 8 | ++------------+----------+ +| 8 | 9 | ++------------+----------+ +| 9 | 10 | ++------------+----------+ +| 10 | 11 | ++------------+----------+ +| 11 | 12 | ++------------+----------+ +| 12 | 13 | ++------------+----------+ +| 13 | 14 | ++------------+----------+ +| 14 | 15 | ++------------+----------+ +| 15 | 16 | ++------------+----------+ +| 16 | 17 | ++------------+----------+ +| 17 | 18 | ++------------+----------+ +| 18 | 19 | ++------------+----------+ +| 19 | 20 | ++------------+----------+ + NonCoding Class --------------- From b29309a5a6edeee544c36980e678fd910db59fa7 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 14:25:09 +0100 Subject: [PATCH 034/236] Format table --- README.rst | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/README.rst b/README.rst index 6a3f600..c84eade 100644 --- a/README.rst +++ b/README.rst @@ -83,49 +83,30 @@ Genomic Position Conversion {"position": 1} >>> crossmap.genomic_to_coordinate({"position": 1}) 0 - Here is the mapping of coordinates to genomic positions: + +------------+----------+ | Coordinate | Position | +============+==========+ | 0 | 1 | -+------------+----------+ | 1 | 2 | -+------------+----------+ | 2 | 3 | -+------------+----------+ | 3 | 4 | -+------------+----------+ | 4 | 5 | -+------------+----------+ | 5 | 6 | -+------------+----------+ | 6 | 7 | -+------------+----------+ | 7 | 8 | -+------------+----------+ | 8 | 9 | -+------------+----------+ | 9 | 10 | -+------------+----------+ | 10 | 11 | -+------------+----------+ | 11 | 12 | -+------------+----------+ | 12 | 13 | -+------------+----------+ | 13 | 14 | -+------------+----------+ | 14 | 15 | -+------------+----------+ | 15 | 16 | -+------------+----------+ | 16 | 17 | -+------------+----------+ | 17 | 18 | -+------------+----------+ | 18 | 19 | -+------------+----------+ | 19 | 20 | +------------+----------+ From d1c4c2ff2190433f34916fb017264df9af461148 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 14:37:26 +0100 Subject: [PATCH 035/236] Format table --- README.rst | 64 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/README.rst b/README.rst index c84eade..f68c424 100644 --- a/README.rst +++ b/README.rst @@ -84,31 +84,45 @@ Genomic Position Conversion >>> crossmap.genomic_to_coordinate({"position": 1}) 0 Here is the mapping of coordinates to genomic positions: - -+------------+----------+ -| Coordinate | Position | -+============+==========+ -| 0 | 1 | -| 1 | 2 | -| 2 | 3 | -| 3 | 4 | -| 4 | 5 | -| 5 | 6 | -| 6 | 7 | -| 7 | 8 | -| 8 | 9 | -| 9 | 10 | -| 10 | 11 | -| 11 | 12 | -| 12 | 13 | -| 13 | 14 | -| 14 | 15 | -| 15 | 16 | -| 16 | 17 | -| 17 | 18 | -| 18 | 19 | -| 19 | 20 | -+------------+----------+ +Here are example mappings for the transcript: + +.. note:: + + These examples use the following data: + + .. code-block:: python + + _exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] + _cds = (32, 43) + +--- + +Genomic Positions +================= + +.. csv-table:: Coordinate to Genomic Position + :header: "Coordinate", "Position" + + 0, 1 + 1, 2 + 2, 3 + 3, 4 + 4, 5 + 5, 6 + 6, 7 + 7, 8 + 8, 9 + 9, 10 + 10, 11 + 11, 12 + 12, 13 + 13, 14 + 14, 15 + 15, 16 + 16, 17 + 17, 18 + 18, 19 + 19, 20 NonCoding Class --------------- From 7966a14d10d0c87cfd1de803da70f26e0ee26dc3 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 14:40:39 +0100 Subject: [PATCH 036/236] Format table --- README.rst | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index f68c424..6b67229 100644 --- a/README.rst +++ b/README.rst @@ -84,21 +84,9 @@ Genomic Position Conversion >>> crossmap.genomic_to_coordinate({"position": 1}) 0 Here is the mapping of coordinates to genomic positions: -Here are example mappings for the transcript: +.. raw:: html -.. note:: - - These examples use the following data: - - .. code-block:: python - - _exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] - _cds = (32, 43) - ---- - -Genomic Positions -================= +
.. csv-table:: Coordinate to Genomic Position :header: "Coordinate", "Position" @@ -124,6 +112,10 @@ Genomic Positions 18, 19 19, 20 +.. raw:: html + +
+ NonCoding Class --------------- From 035c479aa2500310445655c2ef55c8cd13650c77 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 14:59:23 +0100 Subject: [PATCH 037/236] Add tables --- README.rst | 197 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 174 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index 6b67229..4f6f084 100644 --- a/README.rst +++ b/README.rst @@ -84,11 +84,8 @@ Genomic Position Conversion >>> crossmap.genomic_to_coordinate({"position": 1}) 0 Here is the mapping of coordinates to genomic positions: -.. raw:: html -
- -.. csv-table:: Coordinate to Genomic Position +.. csv-table:: Coordinate to Genomic Position (0-4) :header: "Coordinate", "Position" 0, 1 @@ -96,25 +93,7 @@ Here is the mapping of coordinates to genomic positions: 2, 3 3, 4 4, 5 - 5, 6 - 6, 7 - 7, 8 - 8, 9 - 9, 10 - 10, 11 - 11, 12 - 12, 13 - 13, 14 - 14, 15 - 15, 16 - 16, 17 - 17, 18 - 18, 19 - 19, 20 - -.. raw:: html - -
+ ... NonCoding Class --------------- @@ -157,6 +136,92 @@ Notes - Add the flag ``inverted=True`` to the constructor when the transcript resides on the reverse complement strand. +Here is the mapping of coordinates to noncoding positions: +.. csv-table:: Coordinate Mapping + :header: "Coordinate", "Position", "Offset", "Region" + + 0, 5, 0, u + 1, 4, 0, u + 2, 3, 0, u + 3, 2, 0, u + 4, 1, 0, u + 5, 1, 0, + 6, 2, 0, + 7, 3, 0, + 8, 3, 1, + 9, 3, 2, + 10, 3, 3, + 11, 4, -3, + 12, 4, -2, + 13, 4, -1, + 14, 4, 0, + 15, 5, 0, + 16, 6, 0, + 17, 7, 0, + 18, 8, 0, + 19, 9, 0, + 20, 9, 1, + 21, 9, 2, + 22, 9, 3, + 23, 9, 4, + 24, 9, 5, + 25, 10, -5, + 26, 10, -4, + 27, 10, -3, + 28, 10, -2, + 29, 10, -1, + 30, 10, 0, + 31, 11, 0, + 32, 12, 0, + 33, 13, 0, + 34, 14, 0, + 35, 14, 1, + 36, 14, 2, + 37, 14, 3, + 38, 15, -2, + 39, 15, -1, + 40, 15, 0, + 41, 16, 0, + 42, 17, 0, + 43, 18, 0, + 44, 18, 1, + 45, 18, 2, + 46, 18, 3, + 47, 19, -3, + 48, 19, -2, + 49, 19, -1, + 50, 19, 0, + 51, 20, 0, + 52, 20, 1, + 53, 20, 2, + 54, 20, 3, + 55, 20, 4, + 56, 20, 5, + 57, 20, 6, + 58, 20, 7, + 59, 20, 8, + 60, 20, 9, + 61, 21, -9, + 62, 21, -8, + 63, 21, -7, + 64, 21, -6, + 65, 21, -5, + 66, 21, -4, + 67, 21, -3, + 68, 21, -2, + 69, 21, -1, + 70, 21, 0, + 71, 22, 0, + 72, 1, 0, d + 73, 2, 0, d + 74, 3, 0, d + 75, 4, 0, d + 76, 5, 0, d + 77, 6, 0, d + 78, 7, 0, d + 79, 8, 0, d + + Coding Class ------------ @@ -198,6 +263,92 @@ Notes - The flag ``inverted=True`` can be used for transcripts on the reverse complement strand. +Here is the mapping of coordinates to coding positions: + +.. csv-table:: Coordinate Mapping + :header: "Coordinate", "Position", "Offset", "Region" + + 0, 5, 0, u + 1, 4, 0, u + 2, 3, 0, u + 3, 2, 0, u + 4, 1, 0, u + 5, 11, 0, - + 6, 10, 0, - + 7, 9, 0, - + 8, 9, 1, - + 9, 9, 2, - + 10, 9, 3, - + 11, 8, -3, - + 12, 8, -2, - + 13, 8, -1, - + 14, 8, 0, - + 15, 7, 0, - + 16, 6, 0, - + 17, 5, 0, - + 18, 4, 0, - + 19, 3, 0, - + 20, 3, 1, - + 21, 3, 2, - + 22, 3, 3, - + 23, 3, 4, - + 24, 3, 5, - + 25, 2, -5, - + 26, 2, -4, - + 27, 2, -3, - + 28, 2, -2, - + 29, 2, -1, - + 30, 2, 0, - + 31, 1, 0, - + 32, 1, 0, + 33, 2, 0, + 34, 3, 0, + 35, 3, 1, + 36, 3, 2, + 37, 3, 3, + 38, 4, -2, + 39, 4, -1, + 40, 4, 0, + 41, 5, 0, + 42, 6, 0, + 43, 1, 0, * + 44, 1, 1, * + 45, 1, 2, * + 46, 1, 3, * + 47, 2, -3, * + 48, 2, -2, * + 49, 2, -1, * + 50, 2, 0, * + 51, 3, 0, * + 52, 3, 1, * + 53, 3, 2, * + 54, 3, 3, * + 55, 3, 4, * + 56, 3, 5, * + 57, 3, 6, * + 58, 3, 7, * + 59, 3, 8, * + 60, 3, 9, * + 61, 4, -9, * + 62, 4, -8, * + 63, 4, -7, * + 64, 4, -6, * + 65, 4, -5, * + 66, 4, -4, * + 67, 4, -3, * + 68, 4, -2, * + 69, 4, -1, * + 70, 4, 0, * + 71, 5, 0, * + 72, 1, 0, d + 73, 2, 0, d + 74, 3, 0, d + 75, 4, 0, d + 76, 5, 0, d + 77, 6, 0, d + 78, 7, 0, d + 79, 8, 0, d + Protein ------- From f116376749732c8546f7727b889cc87aaff50548 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 15:04:51 +0100 Subject: [PATCH 038/236] Fix table syntax --- README.rst | 135 +++++++++++++++++++++++++++-------------------------- 1 file changed, 68 insertions(+), 67 deletions(-) diff --git a/README.rst b/README.rst index 4f6f084..1783078 100644 --- a/README.rst +++ b/README.rst @@ -137,6 +137,7 @@ Notes - Add the flag ``inverted=True`` to the constructor when the transcript resides on the reverse complement strand. Here is the mapping of coordinates to noncoding positions: + .. csv-table:: Coordinate Mapping :header: "Coordinate", "Position", "Offset", "Region" @@ -145,73 +146,73 @@ Here is the mapping of coordinates to noncoding positions: 2, 3, 0, u 3, 2, 0, u 4, 1, 0, u - 5, 1, 0, - 6, 2, 0, - 7, 3, 0, - 8, 3, 1, - 9, 3, 2, - 10, 3, 3, - 11, 4, -3, - 12, 4, -2, - 13, 4, -1, - 14, 4, 0, - 15, 5, 0, - 16, 6, 0, - 17, 7, 0, - 18, 8, 0, - 19, 9, 0, - 20, 9, 1, - 21, 9, 2, - 22, 9, 3, - 23, 9, 4, - 24, 9, 5, - 25, 10, -5, - 26, 10, -4, - 27, 10, -3, - 28, 10, -2, - 29, 10, -1, - 30, 10, 0, - 31, 11, 0, - 32, 12, 0, - 33, 13, 0, - 34, 14, 0, - 35, 14, 1, - 36, 14, 2, - 37, 14, 3, - 38, 15, -2, - 39, 15, -1, - 40, 15, 0, - 41, 16, 0, - 42, 17, 0, - 43, 18, 0, - 44, 18, 1, - 45, 18, 2, - 46, 18, 3, - 47, 19, -3, - 48, 19, -2, - 49, 19, -1, - 50, 19, 0, - 51, 20, 0, - 52, 20, 1, - 53, 20, 2, - 54, 20, 3, - 55, 20, 4, - 56, 20, 5, - 57, 20, 6, - 58, 20, 7, - 59, 20, 8, - 60, 20, 9, - 61, 21, -9, - 62, 21, -8, - 63, 21, -7, - 64, 21, -6, - 65, 21, -5, - 66, 21, -4, - 67, 21, -3, - 68, 21, -2, - 69, 21, -1, - 70, 21, 0, - 71, 22, 0, + 5, 1, 0, "" + 6, 2, 0, "" + 7, 3, 0, "" + 8, 3, 1, "" + 9, 3, 2, "" + 10, 3, 3, "" + 11, 4, -3, "" + 12, 4, -2, "" + 13, 4, -1, "" + 14, 4, 0, "" + 15, 5, 0, "" + 16, 6, 0, "" + 17, 7, 0, "" + 18, 8, 0, "" + 19, 9, 0, "" + 20, 9, 1, "" + 21, 9, 2, "" + 22, 9, 3, "" + 23, 9, 4, "" + 24, 9, 5, "" + 25, 10, -5, "" + 26, 10, -4, "" + 27, 10, -3, "" + 28, 10, -2, "" + 29, 10, -1, "" + 30, 10, 0, "" + 31, 11, 0, "" + 32, 12, 0, "" + 33, 13, 0, "" + 34, 14, 0, "" + 35, 14, 1, "" + 36, 14, 2, "" + 37, 14, 3, "" + 38, 15, -2, "" + 39, 15, -1, "" + 40, 15, 0, "" + 41, 16, 0, "" + 42, 17, 0, "" + 43, 18, 0, "" + 44, 18, 1, "" + 45, 18, 2, "" + 46, 18, 3, "" + 47, 19, -3, "" + 48, 19, -2, "" + 49, 19, -1, "" + 50, 19, 0, "" + 51, 20, 0, "" + 52, 20, 1, "" + 53, 20, 2, "" + 54, 20, 3, "" + 55, 20, 4, "" + 56, 20, 5, "" + 57, 20, 6, "" + 58, 20, 7, "" + 59, 20, 8, "" + 60, 20, 9, "" + 61, 21, -9, "" + 62, 21, -8, "" + 63, 21, -7, "" + 64, 21, -6, "" + 65, 21, -5, "" + 66, 21, -4, "" + 67, 21, -3, "" + 68, 21, -2, "" + 69, 21, -1, "" + 70, 21, 0, "" + 71, 22, 0, "" 72, 1, 0, d 73, 2, 0, d 74, 3, 0, d From 89787e6b0662bd544d77554fba30107df06e5d19 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 15:20:37 +0100 Subject: [PATCH 039/236] Fix table syntax --- README.rst | 134 ++++++++++++++++++++++++++--------------------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/README.rst b/README.rst index 1783078..3db86a5 100644 --- a/README.rst +++ b/README.rst @@ -274,73 +274,73 @@ Here is the mapping of coordinates to coding positions: 2, 3, 0, u 3, 2, 0, u 4, 1, 0, u - 5, 11, 0, - - 6, 10, 0, - - 7, 9, 0, - - 8, 9, 1, - - 9, 9, 2, - - 10, 9, 3, - - 11, 8, -3, - - 12, 8, -2, - - 13, 8, -1, - - 14, 8, 0, - - 15, 7, 0, - - 16, 6, 0, - - 17, 5, 0, - - 18, 4, 0, - - 19, 3, 0, - - 20, 3, 1, - - 21, 3, 2, - - 22, 3, 3, - - 23, 3, 4, - - 24, 3, 5, - - 25, 2, -5, - - 26, 2, -4, - - 27, 2, -3, - - 28, 2, -2, - - 29, 2, -1, - - 30, 2, 0, - - 31, 1, 0, - - 32, 1, 0, - 33, 2, 0, - 34, 3, 0, - 35, 3, 1, - 36, 3, 2, - 37, 3, 3, - 38, 4, -2, - 39, 4, -1, - 40, 4, 0, - 41, 5, 0, - 42, 6, 0, - 43, 1, 0, * - 44, 1, 1, * - 45, 1, 2, * - 46, 1, 3, * - 47, 2, -3, * - 48, 2, -2, * - 49, 2, -1, * - 50, 2, 0, * - 51, 3, 0, * - 52, 3, 1, * - 53, 3, 2, * - 54, 3, 3, * - 55, 3, 4, * - 56, 3, 5, * - 57, 3, 6, * - 58, 3, 7, * - 59, 3, 8, * - 60, 3, 9, * - 61, 4, -9, * - 62, 4, -8, * - 63, 4, -7, * - 64, 4, -6, * - 65, 4, -5, * - 66, 4, -4, * - 67, 4, -3, * - 68, 4, -2, * - 69, 4, -1, * - 70, 4, 0, * - 71, 5, 0, * + 5, 11, 0, "-" + 6, 10, 0, "-" + 7, 9, 0, "-" + 8, 9, 1, "-" + 9, 9, 2, "-" + 10, 9, 3, "-" + 11, 8, -3, "-" + 12, 8, -2, "-" + 13, 8, -1, "-" + 14, 8, 0, "-" + 15, 7, 0, "-" + 16, 6, 0, "-" + 17, 5, 0, "-" + 18, 4, 0, "-" + 19, 3, 0, "-" + 20, 3, 1, "-" + 21, 3, 2, "-" + 22, 3, 3, "-" + 23, 3, 4, "-" + 24, 3, 5, "-" + 25, 2, -5, "-" + 26, 2, -4, "-" + 27, 2, -3, "-" + 28, 2, -2, "-" + 29, 2, -1, "-" + 30, 2, 0, "-" + 31, 1, 0, "-" + 32, 1, 0, "" + 33, 2, 0, "" + 34, 3, 0, "" + 35, 3, 1, "" + 36, 3, 2, "" + 37, 3, 3, "" + 38, 4, -2, "" + 39, 4, -1, "" + 40, 4, 0, "" + 41, 5, 0, "" + 42, 6, 0, "" + 43, 1, 0, "*" + 44, 1, 1, "*" + 45, 1, 2, "*" + 46, 1, 3, "*" + 47, 2, -3, "*" + 48, 2, -2, "*" + 49, 2, -1, "*" + 50, 2, 0, "*" + 51, 3, 0, "*" + 52, 3, 1, "*" + 53, 3, 2, "*" + 54, 3, 3, "*" + 55, 3, 4, "*" + 56, 3, 5, "*" + 57, 3, 6, "*" + 58, 3, 7, "*" + 59, 3, 8, "*" + 60, 3, 9, "*" + 61, 4, -9, "*" + 62, 4, -8, "*" + 63, 4, -7, "*" + 64, 4, -6, "*" + 65, 4, -5, "*" + 66, 4, -4, "*" + 67, 4, -3, "*" + 68, 4, -2, "*" + 69, 4, -1, "*" + 70, 4, 0, "*" + 71, 5, 0, "*" 72, 1, 0, d 73, 2, 0, d 74, 3, 0, d From 5e601dc5ae6edf5598cb314b7cb2a1ff1be4829c Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 15:25:32 +0100 Subject: [PATCH 040/236] Fix table syntax --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index 3db86a5..ef3fcb1 100644 --- a/README.rst +++ b/README.rst @@ -138,6 +138,10 @@ Notes Here is the mapping of coordinates to noncoding positions: +.. raw:: html + +
+ .. csv-table:: Coordinate Mapping :header: "Coordinate", "Position", "Offset", "Region" @@ -222,6 +226,8 @@ Here is the mapping of coordinates to noncoding positions: 78, 7, 0, d 79, 8, 0, d +.. raw:: html +
Coding Class ------------ From 751bc3b49cba7d04efb38889ca323215fa7b5663 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 15:29:00 +0100 Subject: [PATCH 041/236] Fix table syntax --- README.rst | 119 ++++++++++++++--------------------------------------- 1 file changed, 32 insertions(+), 87 deletions(-) diff --git a/README.rst b/README.rst index ef3fcb1..46f5404 100644 --- a/README.rst +++ b/README.rst @@ -140,94 +140,39 @@ Here is the mapping of coordinates to noncoding positions: .. raw:: html -
+
+ + .. csv-table:: Coordinate Mapping + :header: "Coordinate", "Position", "Offset", "Region" + + 0, 5, 0, u + 1, 4, 0, u + 2, 3, 0, u + 3, 2, 0, u + 4, 1, 0, u + 5, 1, 0, "" + 6, 2, 0, "" + 7, 3, 0, "" + 8, 3, 1, "" + 9, 3, 2, "" + 10, 3, 3, "" + 11, 4, -3, "" + 12, 4, -2, "" + 13, 4, -1, "" + 14, 4, 0, "" + 15, 5, 0, "" + 16, 6, 0, "" + 17, 7, 0, "" + 18, 8, 0, "" + 19, 9, 0, "" + 20, 9, 1, "" + ... + 79, 8, 0, d + + .. raw:: html + +
-.. csv-table:: Coordinate Mapping - :header: "Coordinate", "Position", "Offset", "Region" - - 0, 5, 0, u - 1, 4, 0, u - 2, 3, 0, u - 3, 2, 0, u - 4, 1, 0, u - 5, 1, 0, "" - 6, 2, 0, "" - 7, 3, 0, "" - 8, 3, 1, "" - 9, 3, 2, "" - 10, 3, 3, "" - 11, 4, -3, "" - 12, 4, -2, "" - 13, 4, -1, "" - 14, 4, 0, "" - 15, 5, 0, "" - 16, 6, 0, "" - 17, 7, 0, "" - 18, 8, 0, "" - 19, 9, 0, "" - 20, 9, 1, "" - 21, 9, 2, "" - 22, 9, 3, "" - 23, 9, 4, "" - 24, 9, 5, "" - 25, 10, -5, "" - 26, 10, -4, "" - 27, 10, -3, "" - 28, 10, -2, "" - 29, 10, -1, "" - 30, 10, 0, "" - 31, 11, 0, "" - 32, 12, 0, "" - 33, 13, 0, "" - 34, 14, 0, "" - 35, 14, 1, "" - 36, 14, 2, "" - 37, 14, 3, "" - 38, 15, -2, "" - 39, 15, -1, "" - 40, 15, 0, "" - 41, 16, 0, "" - 42, 17, 0, "" - 43, 18, 0, "" - 44, 18, 1, "" - 45, 18, 2, "" - 46, 18, 3, "" - 47, 19, -3, "" - 48, 19, -2, "" - 49, 19, -1, "" - 50, 19, 0, "" - 51, 20, 0, "" - 52, 20, 1, "" - 53, 20, 2, "" - 54, 20, 3, "" - 55, 20, 4, "" - 56, 20, 5, "" - 57, 20, 6, "" - 58, 20, 7, "" - 59, 20, 8, "" - 60, 20, 9, "" - 61, 21, -9, "" - 62, 21, -8, "" - 63, 21, -7, "" - 64, 21, -6, "" - 65, 21, -5, "" - 66, 21, -4, "" - 67, 21, -3, "" - 68, 21, -2, "" - 69, 21, -1, "" - 70, 21, 0, "" - 71, 22, 0, "" - 72, 1, 0, d - 73, 2, 0, d - 74, 3, 0, d - 75, 4, 0, d - 76, 5, 0, d - 77, 6, 0, d - 78, 7, 0, d - 79, 8, 0, d - -.. raw:: html -
Coding Class ------------ From 9d156c80483a166dcaec27fff965bf51a9873f5d Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 15:31:27 +0100 Subject: [PATCH 042/236] Fix table indent --- README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 46f5404..87e3fc9 100644 --- a/README.rst +++ b/README.rst @@ -138,6 +138,8 @@ Notes Here is the mapping of coordinates to noncoding positions: +Here is the mapping of coordinates to noncoding positions: + .. raw:: html
@@ -171,8 +173,7 @@ Here is the mapping of coordinates to noncoding positions: .. raw:: html -
- + Coding Class ------------ From ebbb90a8c72eb44fc7c835859721a59978ee9fbb Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:37:29 +0100 Subject: [PATCH 043/236] Update README.rst --- README.rst | 66 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/README.rst b/README.rst index 87e3fc9..f366eaa 100644 --- a/README.rst +++ b/README.rst @@ -144,34 +144,34 @@ Here is the mapping of coordinates to noncoding positions:
- .. csv-table:: Coordinate Mapping - :header: "Coordinate", "Position", "Offset", "Region" - - 0, 5, 0, u - 1, 4, 0, u - 2, 3, 0, u - 3, 2, 0, u - 4, 1, 0, u - 5, 1, 0, "" - 6, 2, 0, "" - 7, 3, 0, "" - 8, 3, 1, "" - 9, 3, 2, "" - 10, 3, 3, "" - 11, 4, -3, "" - 12, 4, -2, "" - 13, 4, -1, "" - 14, 4, 0, "" - 15, 5, 0, "" - 16, 6, 0, "" - 17, 7, 0, "" - 18, 8, 0, "" - 19, 9, 0, "" - 20, 9, 1, "" - ... - 79, 8, 0, d - - .. raw:: html +.. csv-table:: Coordinate Mapping to Noncoding + :header: "Coordinate", "Position", "Offset", "Region" + + 0, 5, 0, u + 1, 4, 0, u + 2, 3, 0, u + 3, 2, 0, u + 4, 1, 0, u + 5, 1, 0, "" + 6, 2, 0, "" + 7, 3, 0, "" + 8, 3, 1, "" + 9, 3, 2, "" + 10, 3, 3, "" + 11, 4, -3, "" + 12, 4, -2, "" + 13, 4, -1, "" + 14, 4, 0, "" + 15, 5, 0, "" + 16, 6, 0, "" + 17, 7, 0, "" + 18, 8, 0, "" + 19, 9, 0, "" + 20, 9, 1, "" + ... + 79, 8, 0, d + +.. raw:: html
@@ -218,7 +218,11 @@ Notes Here is the mapping of coordinates to coding positions: -.. csv-table:: Coordinate Mapping +.. raw:: html + +
+ +.. csv-table:: Coordinate Mapping to Coding :header: "Coordinate", "Position", "Offset", "Region" 0, 5, 0, u @@ -302,6 +306,10 @@ Here is the mapping of coordinates to coding positions: 78, 7, 0, d 79, 8, 0, d +.. raw:: html + +
+ Protein ------- From 9ffa9bfc2bb558017b0e25e7cec06123fc688f2a Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:41:32 +0100 Subject: [PATCH 044/236] Make scrollable tables in README --- README.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index f366eaa..8d0bc03 100644 --- a/README.rst +++ b/README.rst @@ -138,13 +138,11 @@ Notes Here is the mapping of coordinates to noncoding positions: -Here is the mapping of coordinates to noncoding positions: - .. raw:: html -
+
-.. csv-table:: Coordinate Mapping to Noncoding +.. csv-table:: :header: "Coordinate", "Position", "Offset", "Region" 0, 5, 0, u @@ -220,7 +218,7 @@ Here is the mapping of coordinates to coding positions: .. raw:: html -
+
.. csv-table:: Coordinate Mapping to Coding :header: "Coordinate", "Position", "Offset", "Region" From 90f3d7192d197fb15145e470887d44e356e508f3 Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:54:25 +0100 Subject: [PATCH 045/236] Update README.rst --- README.rst | 178 ++++++++++++++++++++++++++--------------------------- 1 file changed, 88 insertions(+), 90 deletions(-) diff --git a/README.rst b/README.rst index 8d0bc03..3cafd15 100644 --- a/README.rst +++ b/README.rst @@ -216,97 +216,95 @@ Notes Here is the mapping of coordinates to coding positions: -.. raw:: html - -
- -.. csv-table:: Coordinate Mapping to Coding - :header: "Coordinate", "Position", "Offset", "Region" +
+Coordinate Mapping Table + +:: + + Coordinate Position Offset Region + 0 5 0 u + 1 4 0 u + 2 3 0 u + 3 2 0 u + 4 1 0 u + 5 11 0 - + 6 10 0 - + 7 9 0 - + 8 9 1 - + 9 9 2 - + 10 9 3 - + 11 8 -3 - + 12 8 -2 - + 13 8 -1 - + 14 8 0 - + 15 7 0 - + 16 6 0 - + 17 5 0 - + 18 4 0 - + 19 3 0 - + 20 3 1 - + 21 3 2 - + 22 3 3 - + 23 3 4 - + 24 3 5 - + 25 2 -5 - + 26 2 -4 - + 27 2 -3 - + 28 2 -2 - + 29 2 -1 - + 30 2 0 - + 31 1 0 - + 32 1 0 + 33 2 0 + 34 3 0 + 35 3 1 + 36 3 2 + 37 3 3 + 38 4 -2 + 39 4 -1 + 40 4 0 + 41 5 0 + 42 6 0 + 43 1 0 * + 44 1 1 * + 45 1 2 * + 46 1 3 * + 47 2 -3 * + 48 2 -2 * + 49 2 -1 * + 50 2 0 * + 51 3 0 * + 52 3 1 * + 53 3 2 * + 54 3 3 * + 55 3 4 * + 56 3 5 * + 57 3 6 * + 58 3 7 * + 59 3 8 * + 60 3 9 * + 61 4 -9 * + 62 4 -8 * + 63 4 -7 * + 64 4 -6 * + 65 4 -5 * + 66 4 -4 * + 67 4 -3 * + 68 4 -2 * + 69 4 -1 * + 70 4 0 * + 71 5 0 * + 72 1 0 d + 73 2 0 d + 74 3 0 d + 75 4 0 d + 76 5 0 d + 77 6 0 d + 78 7 0 d + 79 8 0 d + +
- 0, 5, 0, u - 1, 4, 0, u - 2, 3, 0, u - 3, 2, 0, u - 4, 1, 0, u - 5, 11, 0, "-" - 6, 10, 0, "-" - 7, 9, 0, "-" - 8, 9, 1, "-" - 9, 9, 2, "-" - 10, 9, 3, "-" - 11, 8, -3, "-" - 12, 8, -2, "-" - 13, 8, -1, "-" - 14, 8, 0, "-" - 15, 7, 0, "-" - 16, 6, 0, "-" - 17, 5, 0, "-" - 18, 4, 0, "-" - 19, 3, 0, "-" - 20, 3, 1, "-" - 21, 3, 2, "-" - 22, 3, 3, "-" - 23, 3, 4, "-" - 24, 3, 5, "-" - 25, 2, -5, "-" - 26, 2, -4, "-" - 27, 2, -3, "-" - 28, 2, -2, "-" - 29, 2, -1, "-" - 30, 2, 0, "-" - 31, 1, 0, "-" - 32, 1, 0, "" - 33, 2, 0, "" - 34, 3, 0, "" - 35, 3, 1, "" - 36, 3, 2, "" - 37, 3, 3, "" - 38, 4, -2, "" - 39, 4, -1, "" - 40, 4, 0, "" - 41, 5, 0, "" - 42, 6, 0, "" - 43, 1, 0, "*" - 44, 1, 1, "*" - 45, 1, 2, "*" - 46, 1, 3, "*" - 47, 2, -3, "*" - 48, 2, -2, "*" - 49, 2, -1, "*" - 50, 2, 0, "*" - 51, 3, 0, "*" - 52, 3, 1, "*" - 53, 3, 2, "*" - 54, 3, 3, "*" - 55, 3, 4, "*" - 56, 3, 5, "*" - 57, 3, 6, "*" - 58, 3, 7, "*" - 59, 3, 8, "*" - 60, 3, 9, "*" - 61, 4, -9, "*" - 62, 4, -8, "*" - 63, 4, -7, "*" - 64, 4, -6, "*" - 65, 4, -5, "*" - 66, 4, -4, "*" - 67, 4, -3, "*" - 68, 4, -2, "*" - 69, 4, -1, "*" - 70, 4, 0, "*" - 71, 5, 0, "*" - 72, 1, 0, d - 73, 2, 0, d - 74, 3, 0, d - 75, 4, 0, d - 76, 5, 0, d - 77, 6, 0, d - 78, 7, 0, d - 79, 8, 0, d - -.. raw:: html - -
Protein ------- From 93f59ca3864ab4850794f5af4696bda52429689e Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:04:14 +0100 Subject: [PATCH 046/236] Update README.rst --- README.rst | 284 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 166 insertions(+), 118 deletions(-) diff --git a/README.rst b/README.rst index 3cafd15..74a7272 100644 --- a/README.rst +++ b/README.rst @@ -138,40 +138,91 @@ Notes Here is the mapping of coordinates to noncoding positions: -.. raw:: html - -
- .. csv-table:: + :class: table-scroll :header: "Coordinate", "Position", "Offset", "Region" - 0, 5, 0, u - 1, 4, 0, u - 2, 3, 0, u - 3, 2, 0, u - 4, 1, 0, u - 5, 1, 0, "" - 6, 2, 0, "" - 7, 3, 0, "" - 8, 3, 1, "" - 9, 3, 2, "" - 10, 3, 3, "" - 11, 4, -3, "" - 12, 4, -2, "" - 13, 4, -1, "" - 14, 4, 0, "" - 15, 5, 0, "" - 16, 6, 0, "" - 17, 7, 0, "" - 18, 8, 0, "" - 19, 9, 0, "" - 20, 9, 1, "" - ... - 79, 8, 0, d - -.. raw:: html + "0", "5","0", "u" + "1", "4","0", "u" + "2", "3","0", "u" + "3", "2","0", "u" + "4", "1","0", "u" + "5", "1","0", "" + "6", "2","0", "" + "7", "3","0", "" + "8", "3","1", "" + "9", "3","2", "" + "10", "3","3", "" + "11", "4","-3", "" + "12", "4","-2", "" + "13", "4","-1", "" + "14", "4","0", "" + "15", "5","0", "" + "16", "6","0", "" + "17", "7","0", "" + "18", "8","0", "" + "19", "9","0", "" + "20", "9","1", "" + "21", "9","2", "" + "22", "9","3", "" + "23", "9","4", "" + "24", "9","5", "" + "25", "10","-5", "" + "26", "10","-4", "" + "27", "10","-3", "" + "28", "10","-2", "" + "29", "10","-1", "" + "30", "10","0", "" + "31", "11","0", "" + "32", "12","0", "" + "33", "13","0", "" + "34", "14","0", "" + "35", "14","1", "" + "36", "14","2", "" + "37", "14","3", "" + "38", "15","-2", "" + "39", "15","-1", "" + "40", "15","0", "" + "41", "16","0", "" + "42", "17","0", "" + "43", "18","0", "" + "44", "18","1", "" + "45", "18","2", "" + "46", "18","3", "" + "47", "19","-3", "" + "48", "19","-2", "" + "49", "19","-1", "" + "50", "19","0", "" + "51", "20","0", "" + "52", "20","1", "" + "53", "20","2", "" + "54", "20","3", "" + "55", "20","4", "" + "56", "20","5", "" + "57", "20","6", "" + "58", "20","7", "" + "59", "20","8", "" + "60", "20","9", "" + "61", "21","-9", "" + "62", "21","-8", "" + "63", "21","-7", "" + "64", "21","-6", "" + "65", "21","-5", "" + "66", "21","-4", "" + "67", "21","-3", "" + "68", "21","-2", "" + "69", "21","-1", "" + "70", "21","0", "" + "71", "22","0", "" + "72", "1","0", "d" + "73", "2","0", "d" + "74", "3","0", "d" + "75", "4","0", "d" + "76", "5","0", "d" + "77", "6","0", "d" + "78", "7","0", "d" + "79", "8","0", "d" -
Coding Class ------------ @@ -216,94 +267,91 @@ Notes Here is the mapping of coordinates to coding positions: -
-Coordinate Mapping Table - -:: - - Coordinate Position Offset Region - 0 5 0 u - 1 4 0 u - 2 3 0 u - 3 2 0 u - 4 1 0 u - 5 11 0 - - 6 10 0 - - 7 9 0 - - 8 9 1 - - 9 9 2 - - 10 9 3 - - 11 8 -3 - - 12 8 -2 - - 13 8 -1 - - 14 8 0 - - 15 7 0 - - 16 6 0 - - 17 5 0 - - 18 4 0 - - 19 3 0 - - 20 3 1 - - 21 3 2 - - 22 3 3 - - 23 3 4 - - 24 3 5 - - 25 2 -5 - - 26 2 -4 - - 27 2 -3 - - 28 2 -2 - - 29 2 -1 - - 30 2 0 - - 31 1 0 - - 32 1 0 - 33 2 0 - 34 3 0 - 35 3 1 - 36 3 2 - 37 3 3 - 38 4 -2 - 39 4 -1 - 40 4 0 - 41 5 0 - 42 6 0 - 43 1 0 * - 44 1 1 * - 45 1 2 * - 46 1 3 * - 47 2 -3 * - 48 2 -2 * - 49 2 -1 * - 50 2 0 * - 51 3 0 * - 52 3 1 * - 53 3 2 * - 54 3 3 * - 55 3 4 * - 56 3 5 * - 57 3 6 * - 58 3 7 * - 59 3 8 * - 60 3 9 * - 61 4 -9 * - 62 4 -8 * - 63 4 -7 * - 64 4 -6 * - 65 4 -5 * - 66 4 -4 * - 67 4 -3 * - 68 4 -2 * - 69 4 -1 * - 70 4 0 * - 71 5 0 * - 72 1 0 d - 73 2 0 d - 74 3 0 d - 75 4 0 d - 76 5 0 d - 77 6 0 d - 78 7 0 d - 79 8 0 d - -
+.. csv-table:: My Scrollable Table + :class: table-scroll + :header: "Coordinate", "Position", "Offset", "Region" + + "0", "5","0", "u" + "1", "4","0", "u" + "2", "3","0", "u" + "3", "2","0", "u" + "4", "1","0", "u" + "5", "11","0", "-" + "6", "10","0", "-" + "7", "9","0", "-" + "8", "9","1", "-" + "9", "9","2", "-" + "10", "9","3", "-" + "11", "8","-3", "-" + "12", "8","-2", "-" + "13", "8","-1", "-" + "14", "8","0", "-" + "15", "7","0", "-" + "16", "6","0", "-" + "17", "5","0", "-" + "18", "4","0", "-" + "19", "3","0", "-" + "20", "3","1", "-" + "21", "3","2", "-" + "22", "3","3", "-" + "23", "3","4", "-" + "24", "3","5", "-" + "25", "2","-5", "-" + "26", "2","-4", "-" + "27", "2","-3", "-" + "28", "2","-2", "-" + "29", "2","-1", "-" + "30", "2","0", "-" + "31", "1","0", "-" + "32", "1","0", "" + "33", "2","0", "" + "34", "3","0", "" + "35", "3","1", "" + "36", "3","2", "" + "37", "3","3", "" + "38", "4","-2", "" + "39", "4","-1", "" + "40", "4","0", "" + "41", "5","0", "" + "42", "6","0", "" + "43", "1","0", "*" + "44", "1","1", "*" + "45", "1","2", "*" + "46", "1","3", "*" + "47", "2","-3", "*" + "48", "2","-2", "*" + "49", "2","-1", "*" + "50", "2","0", "*" + "51", "3","0", "*" + "52", "3","1", "*" + "53", "3","2", "*" + "54", "3","3", "*" + "55", "3","4", "*" + "56", "3","5", "*" + "57", "3","6", "*" + "58", "3","7", "*" + "59", "3","8", "*" + "60", "3","9", "*" + "61", "4","-9", "*" + "62", "4","-8", "*" + "63", "4","-7", "*" + "64", "4","-6", "*" + "65", "4","-5", "*" + "66", "4","-4", "*" + "67", "4","-3", "*" + "68", "4","-2", "*" + "69", "4","-1", "*" + "70", "4","0", "*" + "71", "5","0", "*" + "72", "1","0", "d" + "73", "2","0", "d" + "74", "3","0", "d" + "75", "4","0", "d" + "76", "5","0", "d" + "77", "6","0", "d" + "78", "7","0", "d" + "79", "8","0", "d" + Protein From 0ef6003debfe9aa6770cfbe46e41f04c07962fad Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 16:13:41 +0100 Subject: [PATCH 047/236] Format table --- README.rst | 120 ++++++++++++++++++++++++++--------------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/README.rst b/README.rst index 74a7272..ee4ab6f 100644 --- a/README.rst +++ b/README.rst @@ -246,7 +246,7 @@ Where: - **position**: a positive integer - **offset**: an integer indicating the offset relative to the position -- **region**: a string describing the region type (``""`` for standard coding positions, ``"-"`` for 5' UTR, ``"*"`` for 3' UTR, ``"u"`` for upstream and ``"d"`` for downstream) +- **region**: a string describing the region type (`""` for standard coding positions, `'-'` for 5' UTR, `'*'` for 3' UTR, `'u'` for upstream and ``"d"`` for downstream) Coding Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -256,8 +256,8 @@ Coding Position Conversion >>> from mutalyzer_crossmapper import Coding >>> crossmap = Coding(_exons, _cds) >>> crossmap.coordinate_to_coding(31) - {"position": -1, "offset": 0, "region": "-"} - >>> crossmap.coding_to_coordinate({"position": -1, "offset": 0, "region": "-"}) + {"position": -1, "offset": 0, "region": '-'} + >>> crossmap.coding_to_coordinate({"position": -1, "offset": 0, "region": '-'}) 31 Notes @@ -270,39 +270,39 @@ Here is the mapping of coordinates to coding positions: .. csv-table:: My Scrollable Table :class: table-scroll :header: "Coordinate", "Position", "Offset", "Region" - + "0", "5","0", "u" "1", "4","0", "u" "2", "3","0", "u" "3", "2","0", "u" "4", "1","0", "u" - "5", "11","0", "-" - "6", "10","0", "-" - "7", "9","0", "-" - "8", "9","1", "-" - "9", "9","2", "-" - "10", "9","3", "-" - "11", "8","-3", "-" - "12", "8","-2", "-" - "13", "8","-1", "-" - "14", "8","0", "-" - "15", "7","0", "-" - "16", "6","0", "-" - "17", "5","0", "-" - "18", "4","0", "-" - "19", "3","0", "-" - "20", "3","1", "-" - "21", "3","2", "-" - "22", "3","3", "-" - "23", "3","4", "-" - "24", "3","5", "-" - "25", "2","-5", "-" - "26", "2","-4", "-" - "27", "2","-3", "-" - "28", "2","-2", "-" - "29", "2","-1", "-" - "30", "2","0", "-" - "31", "1","0", "-" + "5", "11","0", '-' + "6", "10","0", '-' + "7", "9","0", '-' + "8", "9","1", '-' + "9", "9","2", '-' + "10", "9","3", '-' + "11", "8","-3", '-' + "12", "8","-2", '-' + "13", "8","-1", '-' + "14", "8","0", '-' + "15", "7","0", '-' + "16", "6","0", '-' + "17", "5","0", '-' + "18", "4","0", '-' + "19", "3","0", '-' + "20", "3","1", '-' + "21", "3","2", '-' + "22", "3","3", '-' + "23", "3","4", '-' + "24", "3","5", '-' + "25", "2","-5", '-' + "26", "2","-4", '-' + "27", "2","-3", '-' + "28", "2","-2", '-' + "29", "2","-1", '-' + "30", "2","0", '-' + "31", "1","0", '-' "32", "1","0", "" "33", "2","0", "" "34", "3","0", "" @@ -314,35 +314,35 @@ Here is the mapping of coordinates to coding positions: "40", "4","0", "" "41", "5","0", "" "42", "6","0", "" - "43", "1","0", "*" - "44", "1","1", "*" - "45", "1","2", "*" - "46", "1","3", "*" - "47", "2","-3", "*" - "48", "2","-2", "*" - "49", "2","-1", "*" - "50", "2","0", "*" - "51", "3","0", "*" - "52", "3","1", "*" - "53", "3","2", "*" - "54", "3","3", "*" - "55", "3","4", "*" - "56", "3","5", "*" - "57", "3","6", "*" - "58", "3","7", "*" - "59", "3","8", "*" - "60", "3","9", "*" - "61", "4","-9", "*" - "62", "4","-8", "*" - "63", "4","-7", "*" - "64", "4","-6", "*" - "65", "4","-5", "*" - "66", "4","-4", "*" - "67", "4","-3", "*" - "68", "4","-2", "*" - "69", "4","-1", "*" - "70", "4","0", "*" - "71", "5","0", "*" + "43", "1","0", '*' + "44", "1","1", '*' + "45", "1","2", '*' + "46", "1","3", '*' + "47", "2","-3", '*' + "48", "2","-2", '*' + "49", "2","-1", '*' + "50", "2","0", '*' + "51", "3","0", '*' + "52", "3","1", '*' + "53", "3","2", '*' + "54", "3","3", '*' + "55", "3","4", '*' + "56", "3","5", '*' + "57", "3","6", '*' + "58", "3","7", '*' + "59", "3","8", '*' + "60", "3","9", '*' + "61", "4","-9", '*' + "62", "4","-8", '*' + "63", "4","-7", '*' + "64", "4","-6", '*' + "65", "4","-5", '*' + "66", "4","-4", '*' + "67", "4","-3", '*' + "68", "4","-2", '*' + "69", "4","-1", '*' + "70", "4","0", '*' + "71", "5","0", '*' "72", "1","0", "d" "73", "2","0", "d" "74", "3","0", "d" From 2704e52de49a55960aa60f6f67fd2f144ffbd292 Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:30:59 +0100 Subject: [PATCH 048/236] Use code block for table --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index ee4ab6f..7406023 100644 --- a/README.rst +++ b/README.rst @@ -138,10 +138,9 @@ Notes Here is the mapping of coordinates to noncoding positions: -.. csv-table:: - :class: table-scroll - :header: "Coordinate", "Position", "Offset", "Region" +.. code-block:: text + Coordinate, Position, Offset, Region "0", "5","0", "u" "1", "4","0", "u" "2", "3","0", "u" @@ -224,6 +223,7 @@ Here is the mapping of coordinates to noncoding positions: "79", "8","0", "d" + Coding Class ------------ @@ -267,7 +267,7 @@ Notes Here is the mapping of coordinates to coding positions: -.. csv-table:: My Scrollable Table +.. csv-table:: :class: table-scroll :header: "Coordinate", "Position", "Offset", "Region" From 78183fdd586950fae3f971640ff0c7dd70be04fa Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:39:14 +0100 Subject: [PATCH 049/236] Update README.rst --- README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 7406023..afeb981 100644 --- a/README.rst +++ b/README.rst @@ -138,9 +138,10 @@ Notes Here is the mapping of coordinates to noncoding positions: -.. code-block:: text +.. csv-table:: + :class: table-scroll + :header: "Coordinate", "Position", "Offset", "Region" - Coordinate, Position, Offset, Region "0", "5","0", "u" "1", "4","0", "u" "2", "3","0", "u" From 03309a8ac928bfa24c2c35b6318a75407a8cf199 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 10 Mar 2026 22:11:18 +0100 Subject: [PATCH 050/236] Refactor: crossmapper and tests for degenerate option --- mutalyzer_crossmapper/crossmapper.py | 68 ++-- tests/test_crossmapper.py | 571 ++++++++++++++++----------- 2 files changed, 377 insertions(+), 262 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 9762598..5bc9f5c 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -69,46 +69,49 @@ def __init__(self, locations, cds, inverted=False): b0 = self._noncoding.to_position(cds[0]) b1 = self._noncoding.to_position(cds[1]) + e0 = self._noncoding.to_position(locations[0][0]) + e1 = self._noncoding.to_position(locations[-1][1]-1) if self._inverted: self._coding = (b1["position"] + b1["offset"] + 1, b0["position"] + b0["offset"] + 1) - self._cds_len = (b0["position"] + b0["offset"]) - (b1["position"] + b1["offset"]) + self._cds_len = abs((b0["position"] + b0["offset"]) - (b1["position"] + b1["offset"])) + self._exons_end = e0["position"] else: self._coding = (b0["position"] + b0["offset"], b1["position"] + b1["offset"]) - self._cds_len = (b1["position"] + b1["offset"]) - (b0["position"] + b0["offset"]) + self._cds_len = abs((b1["position"] + b1["offset"]) - (b0["position"] + b0["offset"])) + self._exons_end = e1["position"] def _coordinate_to_coding(self, coordinate): """Convert a coordinate to a coding position (c./r.). :arg int coordinate: Coordinate. - :returns tuple: Coding position (c./r.). + :returns dict: Coding position model (c./r.). """ - noncoding_pos = self._noncoding.to_position(coordinate) + noncoding_pos_m = self._noncoding.to_position(coordinate) - # on top of the noncoding position model, add CDs info - location = noncoding_pos["position"] - if noncoding_pos["region"] == "": - if location < self._coding[0]: # before CDs + location = noncoding_pos_m["position"] + if noncoding_pos_m["region"] == "": + if location < self._coding[0]: return { "position": self._coding[0] - location, - "offset": noncoding_pos["offset"], + "offset": noncoding_pos_m["offset"], "region": "-" } - elif location >= self._coding[1]: # after CDs + elif location >= self._coding[1]: return { "position": location - self._coding[1] + 1, - "offset": noncoding_pos["offset"], + "offset": noncoding_pos_m["offset"], "region": "*" } else: return { "position": location - self._coding[0] + 1, - "offset": noncoding_pos["offset"], + "offset": noncoding_pos_m["offset"], "region": "" } else: - return noncoding_pos + return noncoding_pos_m def coordinate_to_coding(self, coordinate, degenerate=False): """Convert a coordinate to a coding position (c./r.). @@ -116,57 +119,58 @@ def coordinate_to_coding(self, coordinate, degenerate=False): :arg int coordinate: Coordinate. :arg bool degenerate: Return a degenerate position. - :returns tuple: Coding position (c./r.). + :returns dict: Coding position model (c./r.). """ - pos = self._coordinate_to_coding(coordinate) - if degenerate and pos["region"] in ["u", "d"]: - if pos["region"] == "u": - pos["position"] = pos["position"] + self._coding[0] - pos["region"] = "-" - else: - pos["position"] = pos["position"] + self._coding[1] - pos["region"] = "*" - return pos + pos_m = self._coordinate_to_coding(coordinate) + if degenerate: + if pos_m["region"] == "u": + pos_m["position"] = pos_m["position"] + self._coding[0] + pos_m["region"] = "-" + if pos_m["region"] == "d": + pos_m["position"] = pos_m["position"] + self._exons_end - self._coding[1] + 1 + pos_m["region"] = "*" + return pos_m def coding_to_coordinate(self, pos_m): """Convert a coding position (c./r.) to a coordinate. - :arg tuple position: Coding position (c./r.). + :arg dict pos_m: Coding position model (c./r.). :returns int: Coordinate. """ region = pos_m["region"] if region == "u": - noncoding_pos = { - "position": abs(pos_m["position"]) + pos_m["offset"], + noncoding_pos_m = { + "position": pos_m["position"] - pos_m["offset"], "offset": 0, "region": "u" } elif region == "d": - noncoding_pos = { - "position": abs(pos_m["position"]) + pos_m["offset"], + noncoding_pos_m = { + "position": pos_m["position"] + pos_m["offset"], "offset": 0, "region": "d" } elif region == "": - noncoding_pos = { + noncoding_pos_m = { "position": pos_m["position"] + self._coding[0] -1, "offset": pos_m["offset"], "region": "" } + # add checks for degenerate results? elif region == "-": - noncoding_pos = { + noncoding_pos_m = { "position": self._coding[0] - pos_m["position"], "offset": pos_m["offset"], "region": "" } else: # * - noncoding_pos = { + noncoding_pos_m = { "position": self._coding[1] + pos_m["position"] - 1, "offset": pos_m["offset"], "region": "" } - return self._noncoding.to_coordinate(noncoding_pos) + return self._noncoding.to_coordinate(noncoding_pos_m) def coordinate_to_protein(self, coordinate): diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 6ba2d83..dbaf87b 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -11,9 +11,17 @@ def test_Genomic(): crossmap = Genomic() invariant( - crossmap.coordinate_to_genomic, 0, crossmap.genomic_to_coordinate, {"position": 1}) + crossmap.coordinate_to_genomic, + 0, + crossmap.genomic_to_coordinate, + {"position": 1}, + ) invariant( - crossmap.coordinate_to_genomic, 98, crossmap.genomic_to_coordinate, {"position": 99}) + crossmap.coordinate_to_genomic, + 98, + crossmap.genomic_to_coordinate, + {"position": 99}, + ) def test_NonCoding(): @@ -22,19 +30,31 @@ def test_NonCoding(): # Boundary between upstream and transcript. invariant( - crossmap.coordinate_to_noncoding, 4, - crossmap.noncoding_to_coordinate, {"position": 1, "offset": 0, "region":"u"}) + crossmap.coordinate_to_noncoding, + 4, + crossmap.noncoding_to_coordinate, + {"position": 1, "offset": 0, "region": "u"}, + ) invariant( - crossmap.coordinate_to_noncoding, 5, - crossmap.noncoding_to_coordinate, {"position": 1, "offset": 0, "region": ""}) + crossmap.coordinate_to_noncoding, + 5, + crossmap.noncoding_to_coordinate, + {"position": 1, "offset": 0, "region": ""}, + ) # Boundary between downstream and transcript. invariant( - crossmap.coordinate_to_noncoding, 71, - crossmap.noncoding_to_coordinate, {"position": 22, "offset": 0, "region": ""}) + crossmap.coordinate_to_noncoding, + 71, + crossmap.noncoding_to_coordinate, + {"position": 22, "offset": 0, "region": ""}, + ) invariant( - crossmap.coordinate_to_noncoding, 72, - crossmap.noncoding_to_coordinate, {"position": 1, "offset": 0, "region": "d"}) + crossmap.coordinate_to_noncoding, + 72, + crossmap.noncoding_to_coordinate, + {"position": 1, "offset": 0, "region": "d"}, + ) def test_NonCoding_inverted(): @@ -43,19 +63,31 @@ def test_NonCoding_inverted(): # Boundary between upstream and transcript. invariant( - crossmap.coordinate_to_noncoding, 72, - crossmap.noncoding_to_coordinate, {"position": 1, "offset": 0, "region": "u"}) + crossmap.coordinate_to_noncoding, + 72, + crossmap.noncoding_to_coordinate, + {"position": 1, "offset": 0, "region": "u"}, + ) invariant( - crossmap.coordinate_to_noncoding, 71, - crossmap.noncoding_to_coordinate, {"position": 1, "offset": 0, "region": ""}) + crossmap.coordinate_to_noncoding, + 71, + crossmap.noncoding_to_coordinate, + {"position": 1, "offset": 0, "region": ""}, + ) # Boundary between downstream and transcript. invariant( - crossmap.coordinate_to_noncoding, 5, - crossmap.noncoding_to_coordinate, {"position": 22, "offset": 0, "region": ""}) + crossmap.coordinate_to_noncoding, + 5, + crossmap.noncoding_to_coordinate, + {"position": 22, "offset": 0, "region": ""}, + ) invariant( - crossmap.coordinate_to_noncoding, 4, - crossmap.noncoding_to_coordinate, {"position": 1, "offset": 0, "region": "d"}) + crossmap.coordinate_to_noncoding, + 4, + crossmap.noncoding_to_coordinate, + {"position": 1, "offset": 0, "region": "d"}, + ) def test_NonCoding_degenerate(): @@ -64,19 +96,23 @@ def test_NonCoding_degenerate(): # Boundary between upstream and transcript. degenerate_equal( - crossmap.noncoding_to_coordinate, 4, + crossmap.noncoding_to_coordinate, + 4, [ - {"position": 1, "offset": 0, "region":"u"}, - {"position": 0, "offset": -1, "region":"u"} - ]) + {"position": 1, "offset": 0, "region": "u"}, + {"position": 0, "offset": -1, "region": "u"}, + ], + ) # Boundary between downstream and transcript. degenerate_equal( - crossmap.noncoding_to_coordinate, 72, + crossmap.noncoding_to_coordinate, + 72, [ {"position": 1, "offset": 0, "region": "d"}, - {"position": 0, "offset": 1, "region": "d"} - ]) + {"position": 0, "offset": 1, "region": "d"}, + ], + ) def test_NonCoding_inverted_degenerate(): @@ -85,54 +121,50 @@ def test_NonCoding_inverted_degenerate(): # Boundary between upstream and transcript. degenerate_equal( - crossmap.noncoding_to_coordinate, 72, - [{"position": 1, "offset": 0, "region": "u"}]) + crossmap.noncoding_to_coordinate, + 72, + [{"position": 1, "offset": 0, "region": "u"}], + ) # Boundary between downstream and transcript. degenerate_equal( - crossmap.noncoding_to_coordinate, 4, - [{"position": 1 , "offset": 0, "region": "d"}]) + crossmap.noncoding_to_coordinate, + 4, + [{"position": 1, "offset": 0, "region": "d"}], + ) + -_exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] -_cds = (32, 43) def test_Coding(): """Forward oriented coding transcript.""" crossmap = Coding(_exons, _cds) # Boundary between 5' and CDS. invariant( - crossmap.coordinate_to_coding, 31, + crossmap.coordinate_to_coding, + 31, crossmap.coding_to_coordinate, - {"position": 1, - "offset":0, - "region":"-" - } + {"position": 1, "offset": 0, "region": "-"}, ) invariant( - crossmap.coordinate_to_coding, 32, + crossmap.coordinate_to_coding, + 32, crossmap.coding_to_coordinate, - {"position": 1, - "offset":0, - "region":"" - } - ) + {"position": 1, "offset": 0, "region": ""}, + ) # Boundary between CDS and 3'. invariant( - crossmap.coordinate_to_coding, 42, + crossmap.coordinate_to_coding, + 42, crossmap.coding_to_coordinate, - {"position": 6, - "offset":0, - "region":"" - }) + {"position": 6, "offset": 0, "region": ""}, + ) invariant( - crossmap.coordinate_to_coding, 43, + crossmap.coordinate_to_coding, + 43, crossmap.coding_to_coordinate, - {"position": 1, - "offset":0, - "region":"*" - } - ) + {"position": 1, "offset": 0, "region": "*"}, + ) def test_Coding_inverted(): @@ -141,42 +173,30 @@ def test_Coding_inverted(): # Boundary between 5' and CDS. invariant( - crossmap.coordinate_to_coding, 43, + crossmap.coordinate_to_coding, + 43, crossmap.coding_to_coordinate, - { - "position": 1, - "offset": 0, - "region": "-" - } + {"position": 1, "offset": 0, "region": "-"}, ) invariant( - crossmap.coordinate_to_coding, 42, + crossmap.coordinate_to_coding, + 42, crossmap.coding_to_coordinate, - { - "position": 1, - "offset": 0, - "region": "" - } - ) + {"position": 1, "offset": 0, "region": ""}, + ) # Boundary between CDS and 3'. invariant( - crossmap.coordinate_to_coding, 32, + crossmap.coordinate_to_coding, + 32, crossmap.coding_to_coordinate, - { - "position": 6, - "offset": 0, - "region": "" - } - ) + {"position": 6, "offset": 0, "region": ""}, + ) invariant( - crossmap.coordinate_to_coding, 31, + crossmap.coordinate_to_coding, + 31, crossmap.coding_to_coordinate, - { - "position": 1, - "offset": 0, - "region": "*" - } + {"position": 1, "offset": 0, "region": "*"}, ) @@ -186,23 +206,31 @@ def test_Coding_regions(): # Upstream odd length intron between two regions. invariant( - crossmap.coordinate_to_coding, 25, + crossmap.coordinate_to_coding, + 25, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 5, 'region': '-'}) + {"position": 1, "offset": 5, "region": "-"}, + ) invariant( - crossmap.coordinate_to_coding, 26, + crossmap.coordinate_to_coding, + 26, crossmap.coding_to_coordinate, - {'position': 1, 'offset': -4, 'region': ''}) + {"position": 1, "offset": -4, "region": ""}, + ) # Downstream odd length intron between two regions. invariant( - crossmap.coordinate_to_coding, 44, + crossmap.coordinate_to_coding, + 44, crossmap.coding_to_coordinate, - {'position': 10, 'offset': 5, 'region': ''}) + {"position": 10, "offset": 5, "region": ""}, + ) invariant( - crossmap.coordinate_to_coding, 45, + crossmap.coordinate_to_coding, + 45, crossmap.coding_to_coordinate, - {'position': 1, 'offset': -4, 'region': '*'}) + {"position": 1, "offset": -4, "region": "*"}, + ) def test_Coding_regions_inverted(): @@ -211,23 +239,31 @@ def test_Coding_regions_inverted(): # Upstream odd length intron between two regions. invariant( - crossmap.coordinate_to_coding, 44, + crossmap.coordinate_to_coding, + 44, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 5, 'region': '-'}) + {"position": 1, "offset": 5, "region": "-"}, + ) invariant( - crossmap.coordinate_to_coding, 43, + crossmap.coordinate_to_coding, + 43, crossmap.coding_to_coordinate, - {'position': 1, 'offset': -4, 'region': ''}) + {"position": 1, "offset": -4, "region": ""}, + ) # Downstream odd length intron between two regions. invariant( - crossmap.coordinate_to_coding, 25, + crossmap.coordinate_to_coding, + 25, crossmap.coding_to_coordinate, - {'position': 10, 'offset': 5, 'region': ''}) + {"position": 10, "offset": 5, "region": ""}, + ) invariant( - crossmap.coordinate_to_coding, 24, + crossmap.coordinate_to_coding, + 24, crossmap.coding_to_coordinate, - {'position': 1, 'offset': -4, 'region': '*'}) + {"position": 1, "offset": -4, "region": "*"}, + ) def test_Coding_no_utr5(): @@ -236,13 +272,17 @@ def test_Coding_no_utr5(): # Direct transition from upstream to CDS. invariant( - crossmap.coordinate_to_coding, 9, + crossmap.coordinate_to_coding, + 9, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'u'}) # serialize result : u1 + {"position": 1, "offset": 0, "region": "u"}, + ) invariant( - crossmap.coordinate_to_coding, 10, + crossmap.coordinate_to_coding, + 10, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': ''}) # serialize result: 1 + {"position": 1, "offset": 0, "region": ""}, + ) def test_Coding_no_utr5_inverted(): @@ -251,13 +291,17 @@ def test_Coding_no_utr5_inverted(): # Direct transition from upstream to CDS. invariant( - crossmap.coordinate_to_coding, 20, + crossmap.coordinate_to_coding, + 20, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'u'}) + {"position": 1, "offset": 0, "region": "u"}, + ) invariant( - crossmap.coordinate_to_coding, 19, + crossmap.coordinate_to_coding, + 19, crossmap.coding_to_coordinate, - {'position': 2, 'offset': 0, 'region': '-'}) + {"position": 2, "offset": 0, "region": "-"}, + ) def test_Coding_no_utr3(): @@ -265,15 +309,18 @@ def test_Coding_no_utr3(): crossmap = Coding([(10, 20)], (15, 20)) # Direct transition from CDS to downstream. - #TODO: invariant( - crossmap.coordinate_to_coding, 19, + crossmap.coordinate_to_coding, + 19, crossmap.coding_to_coordinate, - {'position': 9, 'offset': 0, 'region': '*'}) + {"position": 9, "offset": 0, "region": "*"}, + ) invariant( - crossmap.coordinate_to_coding, 20, + crossmap.coordinate_to_coding, + 20, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'd'}) + {"position": 1, "offset": 0, "region": "d"}, + ) def test_Coding_no_utr3_inverted(): @@ -282,13 +329,17 @@ def test_Coding_no_utr3_inverted(): # Direct transition from CDS to downstream. invariant( - crossmap.coordinate_to_coding, 10, + crossmap.coordinate_to_coding, + 10, crossmap.coding_to_coordinate, - {'position': 5, 'offset': 0, 'region': ''}) + {"position": 5, "offset": 0, "region": ""}, + ) invariant( - crossmap.coordinate_to_coding, 9, + crossmap.coordinate_to_coding, + 9, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'd'}) + {"position": 1, "offset": 0, "region": "d"}, + ) def test_Coding_small_utr5(): @@ -297,18 +348,23 @@ def test_Coding_small_utr5(): # Transition from upstream to 5' UTR to CDS. invariant( - crossmap.coordinate_to_coding, 9, + crossmap.coordinate_to_coding, + 9, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'u'}) + {"position": 1, "offset": 0, "region": "u"}, + ) invariant( - crossmap.coordinate_to_coding, 10, + crossmap.coordinate_to_coding, + 10, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': '-'} + {"position": 1, "offset": 0, "region": "-"}, ) invariant( - crossmap.coordinate_to_coding, 11, + crossmap.coordinate_to_coding, + 11, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': ''}) + {"position": 1, "offset": 0, "region": ""}, + ) def test_Coding_small_utr5_inverted(): @@ -317,17 +373,23 @@ def test_Coding_small_utr5_inverted(): # Transition from upstream to 5' UTR to CDS. invariant( - crossmap.coordinate_to_coding, 20, - crossmap.coding_to_coordinate,# (-1, -1, -1, -1) - {'position': 1, 'offset': 0, 'region': 'u'}) + crossmap.coordinate_to_coding, + 20, + crossmap.coding_to_coordinate, + {"position": 1, "offset": 0, "region": "u"}, + ) invariant( - crossmap.coordinate_to_coding, 19, - crossmap.coding_to_coordinate, #(-1, 0, -1, 0)) - {'position': 1, 'offset': 0, 'region': '-'}) + crossmap.coordinate_to_coding, + 19, + crossmap.coding_to_coordinate, + {"position": 1, "offset": 0, "region": "-"}, + ) invariant( - crossmap.coordinate_to_coding, 18, + crossmap.coordinate_to_coding, + 18, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': ''}) + {"position": 1, "offset": 0, "region": ""}, + ) def test_Coding_small_utr3(): @@ -336,17 +398,23 @@ def test_Coding_small_utr3(): # Transition from CDS to 3' UTR to downstream. invariant( - crossmap.coordinate_to_coding, 18, + crossmap.coordinate_to_coding, + 18, crossmap.coding_to_coordinate, - {'position': 4, 'offset': 0, 'region': ''}) + {"position": 4, "offset": 0, "region": ""}, + ) invariant( - crossmap.coordinate_to_coding, 19, - crossmap.coding_to_coordinate, #(1, 0, 1, 0) - {'position': 1, 'offset': 0, 'region': '*'}) + crossmap.coordinate_to_coding, + 19, + crossmap.coding_to_coordinate, + {"position": 1, "offset": 0, "region": "*"}, + ) invariant( - crossmap.coordinate_to_coding, 20, - crossmap.coding_to_coordinate, #(1, 1, 1, 1)) - {'position': 1, 'offset': 0, 'region': 'd'}) + crossmap.coordinate_to_coding, + 20, + crossmap.coding_to_coordinate, + {"position": 1, "offset": 0, "region": "d"}, + ) def test_Coding_small_utr3_inverted(): @@ -355,17 +423,23 @@ def test_Coding_small_utr3_inverted(): # Transition from CDS to 3' UTR to downstream. invariant( - crossmap.coordinate_to_coding, 11, + crossmap.coordinate_to_coding, + 11, crossmap.coding_to_coordinate, - {'position': 4, 'offset': 0, 'region': ''}) + {"position": 4, "offset": 0, "region": ""}, + ) invariant( - crossmap.coordinate_to_coding, 10, + crossmap.coordinate_to_coding, + 10, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': '*'}) + {"position": 1, "offset": 0, "region": "*"}, + ) invariant( - crossmap.coordinate_to_coding, 9, + crossmap.coordinate_to_coding, + 9, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'd'}) + {"position": 1, "offset": 0, "region": "d"}, + ) def test_Coding_degenerate(): @@ -373,57 +447,45 @@ def test_Coding_degenerate(): crossmap = Coding([(10, 20)], (11, 19)) degenerate_equal( - crossmap.coding_to_coordinate, 9, + crossmap.coding_to_coordinate, + 9, [ - {'position': 1, 'offset': 0, 'region': 'u'}, - {'position': 2, 'offset': 1, 'region': 'u'}, - {'position': 0, 'offset': -1, 'region': 'u'}, - {'position': 1, 'offset': -1, 'region': '-'}, - {'position': 2, 'offset': 0, 'region': '-'}, - {'position': 1, 'offset': -2, 'region': ''}, - ]) + {"position": 1, "offset": 0, "region": "u"}, + {"position": 2, "offset": 1, "region": "u"}, + {"position": 0, "offset": -1, "region": "u"}, + ], + ) degenerate_equal( - crossmap.coding_to_coordinate, 20, + crossmap.coding_to_coordinate, + 20, [ - {'position': 1, 'offset': 0, 'region': 'd'}, - {'position': 8, 'offset': -7, 'region': 'd'}, - {'position': 0, 'offset': -1, 'region': 'd'}, - {'position': 2, 'offset': 0, 'region': '*'}, - {'position': 1, 'offset': 1, 'region': '*'}, - {'position': 8, 'offset': 2, 'region': ''}, - ] + {"position": 1, "offset": 0, "region": "d"}, + {"position": 8, "offset": -7, "region": "d"}, + {"position": 0, "offset": -1, "region": "d"}, + ], ) -#TODO: Add tests for silently degenerate, -# position value <= 0 -# offset value > intron length - - def test_Coding_inverted_degenerate(): """Degenerate upstream and downstream positions are silently corrected.""" crossmap = Coding([(10, 20)], (11, 19), True) degenerate_equal( - crossmap.coding_to_coordinate, 20, + crossmap.coding_to_coordinate, + 20, [ - {'position': 1, 'offset': 0, 'region': 'u'}, - {'position': 2, 'offset': 1, 'region': 'u'}, - {'position': 0, 'offset': -1, 'region': 'u'}, - {'position': 1, 'offset': -2, 'region': ''}, - {'position': 1, 'offset': -1, 'region': '-'}, - {'position': 2, 'offset': 0, 'region': '-'} - ] + {"position": 1, "offset": 0, "region": "u"}, + {"position": 2, "offset": 1, "region": "u"}, + {"position": 0, "offset": -1, "region": "u"}, + ], ) degenerate_equal( - crossmap.coding_to_coordinate, 9, + crossmap.coding_to_coordinate, + 9, [ - {'position': 1, 'offset': 0, 'region': 'd'}, - {'position': 2, 'offset': -1, 'region': 'd'}, - {'position': 1, 'offset': 1, 'region': '*'}, - {'position': 1, 'offset': 1, 'region': '*'}, - {'position': 10, 'offset': 0, 'region': ''}, - ] + {"position": 1, "offset": 0, "region": "d"}, + {"position": 2, "offset": -1, "region": "d"}, + ], ) @@ -431,37 +493,50 @@ def test_Coding_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" crossmap = Coding([(10, 20)], (11, 19)) - for i in range(0, 30): - print(i, crossmap.coordinate_to_coding(i), crossmap.coordinate_to_coding(i, True)) - - assert crossmap.coordinate_to_coding(9, True) == {'position': 2, 'offset': 0, 'region': '-'} - assert crossmap.coordinate_to_coding(20, True) == {'position': 2, 'offset': 0, 'region': '*'} + assert crossmap.coordinate_to_coding(9, True) == { + "position": 2, + "offset": 0, + "region": "-", + } + assert crossmap.coordinate_to_coding(20, True) == { + "position": 2, + "offset": 0, + "region": "*", + } def test_Coding_inverted_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" crossmap = Coding([(10, 20)], (11, 19), True) for i in range(0, 30): - print(i, crossmap.coordinate_to_coding(i), crossmap.coordinate_to_coding(i, True)) + print( + i, crossmap.coordinate_to_coding(i), crossmap.coordinate_to_coding(i, True) + ) - assert crossmap.coordinate_to_coding(20, True) == {'position': 2, 'offset': 0, 'region': '-'} - assert crossmap.coordinate_to_coding(9, True) == {'position': 2, 'offset': 0, 'region': '*'} + assert crossmap.coordinate_to_coding(20, True) == { + "position": 2, + "offset": 0, + "region": "-", + } + assert crossmap.coordinate_to_coding(9, True) == { + "position": 2, + "offset": 0, + "region": "*", + } def test_Coding_degenerate_no_return(): """Degenerate internal positions do not exist.""" crossmap = Coding([(10, 20), (30, 40)], (10, 40)) - assert (crossmap.coordinate_to_coding(25) == - crossmap.coordinate_to_coding(25, True)) + assert crossmap.coordinate_to_coding(25) == crossmap.coordinate_to_coding(25, True) def test_Coding_inverted_degenerate_no_return(): """Degenerate internal positions do not exist.""" crossmap = Coding([(10, 20), (30, 40)], (10, 40), True) - assert (crossmap.coordinate_to_coding(25) == - crossmap.coordinate_to_coding(25, True)) + assert crossmap.coordinate_to_coding(25) == crossmap.coordinate_to_coding(25, True) def test_Coding_no_utr_degenerate(): @@ -469,45 +544,45 @@ def test_Coding_no_utr_degenerate(): crossmap = Coding([(10, 11)], (10, 11)) degenerate_equal( - crossmap.coding_to_coordinate, 9, + crossmap.coding_to_coordinate, + 9, [ - {'position': 1, 'offset': 0, 'region': '-'}, - {'position': 1, 'offset': 0, 'region': 'u'}, - {'position': 1, 'offset': -1, 'region': ''}, - ] + {"position": 2, "offset": 1, "region": "u"}, + {"position": 1, "offset": 0, "region": "u"}, + ], ) degenerate_equal( - crossmap.coding_to_coordinate, 11, + crossmap.coding_to_coordinate, + 11, [ - {'position': 1, 'offset': 0, 'region': '*'}, - {'position': 1, 'offset': 0, 'region': 'd'}, - {'position': 1, 'offset': 1, 'region': ''} - ] + {"position": 1, "offset": 0, "region": "d"}, + {"position": 2, "offset": -1, "region": "d"}, + ], ) + def test_Coding_inverted_no_utr_degenerate(): """UTRs may be missing.""" crossmap = Coding([(10, 11)], (10, 11), True) - # [(1, -1, 0, -1), (-1, 0, -1, -1), (1, -2, 1, -1)]) degenerate_equal( - crossmap.coding_to_coordinate, 11, + crossmap.coding_to_coordinate, + 11, [ - {'position': 1, 'offset': 0, 'region': 'u'}, - {'position': 2, 'offset': 1, 'region': 'u'}, - {'position': 1, 'offset': 0, 'region': '-'}, - {'position': 1, 'offset': 0, 'region': '*'}, - ] -) + {"position": 1, "offset": 0, "region": "u"}, + {"position": 2, "offset": 1, "region": "u"}, + ], + ) degenerate_equal( - crossmap.coding_to_coordinate, 9, + crossmap.coding_to_coordinate, + 9, [ - {'position': 1, 'offset': 0, 'region': 'd'}, - {'position': 1, 'offset': 0, 'region': '*'}, - {'position': 1, 'offset': -1, 'region': ''}, - ] + {"position": 1, "offset": 0, "region": "d"}, + {"position": 2, "offset": -1, "region": "d"}, + ], ) + def test_Coding_no_utr_degenerate_return(): """UTRs may be missing.""" crossmap = Coding([(10, 11)], (10, 11)) @@ -515,50 +590,86 @@ def test_Coding_no_utr_degenerate_return(): print(crossmap.coordinate_to_coding(11), crossmap.coordinate_to_coding(11, True)) print(crossmap.coordinate_to_coding(12), crossmap.coordinate_to_coding(12, True)) - assert crossmap.coordinate_to_coding(8, True) == {'position': 2, 'offset': 0, 'region': '-'}#(-2, 0, -1, -2) - assert crossmap.coordinate_to_coding(9, True) == {'position': 1, 'offset': 0, 'region': '-'}#(-1, 0, -1, -1) - assert crossmap.coordinate_to_coding(11, True) == {'position': 1, 'offset': 0, 'region': '*'}#(1, 0, 1, 1) - assert crossmap.coordinate_to_coding(12, True) == {'position': 2, 'offset': 0, 'region': '*'}#(2, 0, 1, 2) + assert crossmap.coordinate_to_coding(8, True) == { + "position": 2, + "offset": 0, + "region": "-", + } + assert crossmap.coordinate_to_coding(9, True) == { + "position": 1, + "offset": 0, + "region": "-", + } + assert crossmap.coordinate_to_coding(11, True) == { + "position": 1, + "offset": 0, + "region": "*", + } + assert crossmap.coordinate_to_coding(12, True) == { + "position": 2, + "offset": 0, + "region": "*", + } def test_Coding_inverted_no_utr_degenerate_return(): """UTRs may be missing.""" crossmap = Coding([(10, 11)], (10, 11), True) - assert crossmap.coordinate_to_coding(11, True) == (-1, 0, -1, -1) - assert crossmap.coordinate_to_coding(9, True) == (1, 0, 1, 1) + assert crossmap.coordinate_to_coding(11, True) == { + "position": 3, + "offset": 0, + "region": "-", + } + assert crossmap.coordinate_to_coding(9, True) == { + "position": 1, + "offset": 0, + "region": "*", + } def test_Coding_protein(): """Protein positions.""" crossmap = Coding(_exons, _cds) - # Boundary between 5' UTR and CDS. + # Boundary between 5' UTR and CDS invariant( - crossmap.coordinate_to_protein, 31, + crossmap.coordinate_to_protein, + 31, crossmap.protein_to_coordinate, - {'position': 1, "position_in_codon": 1, 'offset': 0, 'region': '-'}) + {"position": 1, "position_in_codon": 1, "offset": 0, "region": "-"}, + ) invariant( - crossmap.coordinate_to_protein, 32, + crossmap.coordinate_to_protein, + 32, crossmap.protein_to_coordinate, - {'position': 1, "position_in_codon": 1, 'offset': 0, 'region': ''}) + {"position": 1, "position_in_codon": 1, "offset": 0, "region": ""}, + ) # Intron boundary. invariant( - crossmap.coordinate_to_protein, 34, + crossmap.coordinate_to_protein, + 34, crossmap.protein_to_coordinate, - {'position': 1, "position_in_codon": 3, 'offset': 0, 'region': ''}) + {"position": 1, "position_in_codon": 3, "offset": 0, "region": ""}, + ) invariant( - crossmap.coordinate_to_protein, 35, + crossmap.coordinate_to_protein, + 35, crossmap.protein_to_coordinate, - {'position': 1, "position_in_codon": 3, 'offset': 1, 'region': ''}) + {"position": 1, "position_in_codon": 3, "offset": 1, "region": ""}, + ) # Boundary between CDS and 3' UTR. invariant( - crossmap.coordinate_to_protein, 42, + crossmap.coordinate_to_protein, + 42, crossmap.protein_to_coordinate, - {'position': 2, "position_in_codon": 3, 'offset': 0, 'region': ''}) + {"position": 2, "position_in_codon": 3, "offset": 0, "region": ""}, + ) invariant( - crossmap.coordinate_to_protein, 43, + crossmap.coordinate_to_protein, + 43, crossmap.protein_to_coordinate, - {'position': 1, "position_in_codon": 1, 'offset': 0, 'region': '*'}) + {"position": 1, "position_in_codon": 1, "offset": 0, "region": "*"}, + ) From fccc27555324864800126ea59c7b178ada24b68c Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 11 Mar 2026 08:53:11 +0100 Subject: [PATCH 051/236] Add surpport for degenerate position model as input --- mutalyzer_crossmapper/crossmapper.py | 55 +++++++++++++++++++--------- tests/test_crossmapper.py | 13 +++---- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 5bc9f5c..65b07d9 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -68,18 +68,20 @@ def __init__(self, locations, cds, inverted=False): NonCoding.__init__(self, locations, inverted) b0 = self._noncoding.to_position(cds[0]) - b1 = self._noncoding.to_position(cds[1]) + b1 = self._noncoding.to_position(cds[1]-1) e0 = self._noncoding.to_position(locations[0][0]) e1 = self._noncoding.to_position(locations[-1][1]-1) if self._inverted: - self._coding = (b1["position"] + b1["offset"] + 1, b0["position"] + b0["offset"] + 1) + self._coding = (b1["position"] + b1["offset"], b0["position"] + b0["offset"] + 1) self._cds_len = abs((b0["position"] + b0["offset"]) - (b1["position"] + b1["offset"])) - self._exons_end = e0["position"] + self._exons_end = e1["position"] + self._exons_start = e0["position"] else: - self._coding = (b0["position"] + b0["offset"], b1["position"] + b1["offset"]) + self._coding = (b0["position"] + b0["offset"], b1["position"] + b1["offset"] +1) self._cds_len = abs((b1["position"] + b1["offset"]) - (b0["position"] + b0["offset"])) self._exons_end = e1["position"] + self._exons_start = e0["position"] def _coordinate_to_coding(self, coordinate): """Convert a coordinate to a coding position (c./r.). @@ -89,7 +91,6 @@ def _coordinate_to_coding(self, coordinate): :returns dict: Coding position model (c./r.). """ noncoding_pos_m = self._noncoding.to_position(coordinate) - location = noncoding_pos_m["position"] if noncoding_pos_m["region"] == "": if location < self._coding[0]: @@ -124,10 +125,16 @@ def coordinate_to_coding(self, coordinate, degenerate=False): pos_m = self._coordinate_to_coding(coordinate) if degenerate: if pos_m["region"] == "u": - pos_m["position"] = pos_m["position"] + self._coding[0] + if self._inverted: + pos_m["position"] = pos_m["position"] + self._exons_start - self._coding[1] + 1 + else: + pos_m["position"] = pos_m["position"] + self._coding[0] pos_m["region"] = "-" if pos_m["region"] == "d": - pos_m["position"] = pos_m["position"] + self._exons_end - self._coding[1] + 1 + if self._inverted: + pos_m["position"] = pos_m["position"] + self._coding[0] + else: + pos_m["position"] = pos_m["position"] + self._exons_end - self._coding[1] + 1 pos_m["region"] = "*" return pos_m @@ -159,17 +166,31 @@ def coding_to_coordinate(self, pos_m): } # add checks for degenerate results? elif region == "-": - noncoding_pos_m = { - "position": self._coding[0] - pos_m["position"], - "offset": pos_m["offset"], - "region": "" - } + if pos_m["position"] > self._coding[0]: #degenerate result + noncoding_pos_m = { + "position": pos_m["position"] - self._coding[0], + "offset": pos_m["offset"], + "region": "u" + } + else: + noncoding_pos_m = { + "position": self._coding[0] - pos_m["position"], + "offset": pos_m["offset"], + "region": "" + } else: # * - noncoding_pos_m = { - "position": self._coding[1] + pos_m["position"] - 1, - "offset": pos_m["offset"], - "region": "" - } + if pos_m["position"] > self._coding[1]: + noncoding_pos_m = { + "position": pos_m["position"] - self._coding[1], + "offset": pos_m["offset"], + "region": "d" + } + else: + noncoding_pos_m = { + "position": self._coding[1] + pos_m["position"] - 1, + "offset": pos_m["offset"], + "region": "" + } return self._noncoding.to_coordinate(noncoding_pos_m) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index dbaf87b..e2cccbe 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -300,7 +300,7 @@ def test_Coding_no_utr5_inverted(): crossmap.coordinate_to_coding, 19, crossmap.coding_to_coordinate, - {"position": 2, "offset": 0, "region": "-"}, + {"position": 1 , "offset": 0, "region": ""}, ) @@ -313,7 +313,7 @@ def test_Coding_no_utr3(): crossmap.coordinate_to_coding, 19, crossmap.coding_to_coordinate, - {"position": 9, "offset": 0, "region": "*"}, + {"position": 5, "offset": 0, "region": ""}, ) invariant( crossmap.coordinate_to_coding, @@ -508,10 +508,7 @@ def test_Coding_degenerate_return(): def test_Coding_inverted_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" crossmap = Coding([(10, 20)], (11, 19), True) - for i in range(0, 30): - print( - i, crossmap.coordinate_to_coding(i), crossmap.coordinate_to_coding(i, True) - ) + assert crossmap.coordinate_to_coding(20, True) == { "position": 2, @@ -571,6 +568,8 @@ def test_Coding_inverted_no_utr_degenerate(): [ {"position": 1, "offset": 0, "region": "u"}, {"position": 2, "offset": 1, "region": "u"}, + {"position": 1, "offset": 0, "region": "-"}, + ], ) degenerate_equal( @@ -617,7 +616,7 @@ def test_Coding_inverted_no_utr_degenerate_return(): crossmap = Coding([(10, 11)], (10, 11), True) assert crossmap.coordinate_to_coding(11, True) == { - "position": 3, + "position": 1, "offset": 0, "region": "-", } From 00e98bdc7df4643ac3eef909555e5338f8edf7b8 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 11 Mar 2026 09:17:46 +0100 Subject: [PATCH 052/236] Add degenerate tests --- tests/test_crossmapper.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index e2cccbe..788863b 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -477,6 +477,7 @@ def test_Coding_inverted_degenerate(): {"position": 1, "offset": 0, "region": "u"}, {"position": 2, "offset": 1, "region": "u"}, {"position": 0, "offset": -1, "region": "u"}, + {"position": 2, "offset": 0, "region": "-"}, ], ) degenerate_equal( @@ -485,6 +486,7 @@ def test_Coding_inverted_degenerate(): [ {"position": 1, "offset": 0, "region": "d"}, {"position": 2, "offset": -1, "region": "d"}, + {"position": 2, "offset": 0, "region": "*"}, ], ) @@ -546,6 +548,7 @@ def test_Coding_no_utr_degenerate(): [ {"position": 2, "offset": 1, "region": "u"}, {"position": 1, "offset": 0, "region": "u"}, + {"position": 1, "offset": 0, "region": "-"}, ], ) degenerate_equal( @@ -554,6 +557,7 @@ def test_Coding_no_utr_degenerate(): [ {"position": 1, "offset": 0, "region": "d"}, {"position": 2, "offset": -1, "region": "d"}, + {"position": 1, "offset": 0, "region": "*"}, ], ) @@ -578,6 +582,8 @@ def test_Coding_inverted_no_utr_degenerate(): [ {"position": 1, "offset": 0, "region": "d"}, {"position": 2, "offset": -1, "region": "d"}, + {"position": 1, "offset": 0, "region": "*"}, + {"position": 1, "offset": 1, "region": ""}, ], ) @@ -586,9 +592,6 @@ def test_Coding_no_utr_degenerate_return(): """UTRs may be missing.""" crossmap = Coding([(10, 11)], (10, 11)) - print(crossmap.coordinate_to_coding(11), crossmap.coordinate_to_coding(11, True)) - print(crossmap.coordinate_to_coding(12), crossmap.coordinate_to_coding(12, True)) - assert crossmap.coordinate_to_coding(8, True) == { "position": 2, "offset": 0, From e281d6423fc2b62f71b7a3ff55d961bb96417fb0 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 11 Mar 2026 09:51:59 +0100 Subject: [PATCH 053/236] Cleanup --- mutalyzer_crossmapper/crossmapper.py | 31 +++++++++++++++------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 65b07d9..ff2369a 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -146,49 +146,52 @@ def coding_to_coordinate(self, pos_m): :returns int: Coordinate. """ region = pos_m["region"] + position = pos_m["position"] + offset = pos_m["offset"] + if region == "u": noncoding_pos_m = { - "position": pos_m["position"] - pos_m["offset"], + "position": position - offset, "offset": 0, "region": "u" } elif region == "d": noncoding_pos_m = { - "position": pos_m["position"] + pos_m["offset"], + "position": position + offset, "offset": 0, "region": "d" } elif region == "": noncoding_pos_m = { - "position": pos_m["position"] + self._coding[0] -1, - "offset": pos_m["offset"], + "position": position + self._coding[0] -1, + "offset": offset, "region": "" } # add checks for degenerate results? elif region == "-": - if pos_m["position"] > self._coding[0]: #degenerate result + if position > self._coding[0]: noncoding_pos_m = { - "position": pos_m["position"] - self._coding[0], - "offset": pos_m["offset"], + "position": position - self._coding[0], + "offset": offset, "region": "u" } else: noncoding_pos_m = { - "position": self._coding[0] - pos_m["position"], - "offset": pos_m["offset"], + "position": self._coding[0] - position, + "offset": offset, "region": "" } else: # * - if pos_m["position"] > self._coding[1]: + if position > self._coding[1]: noncoding_pos_m = { - "position": pos_m["position"] - self._coding[1], - "offset": pos_m["offset"], + "position": position - self._coding[1], + "offset": offset, "region": "d" } else: noncoding_pos_m = { - "position": self._coding[1] + pos_m["position"] - 1, - "offset": pos_m["offset"], + "position": self._coding[1] + position - 1, + "offset": offset, "region": "" } return self._noncoding.to_coordinate(noncoding_pos_m) From 925cb568c0af9e44cd59a9bccb9eed145432498e Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 11 Mar 2026 09:54:13 +0100 Subject: [PATCH 054/236] Cleanup --- mutalyzer_crossmapper/crossmapper.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index ff2369a..c16fdf7 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -74,12 +74,12 @@ def __init__(self, locations, cds, inverted=False): if self._inverted: self._coding = (b1["position"] + b1["offset"], b0["position"] + b0["offset"] + 1) - self._cds_len = abs((b0["position"] + b0["offset"]) - (b1["position"] + b1["offset"])) + self._cds_len = (b0["position"] + b0["offset"]) - (b1["position"] + b1["offset"]) self._exons_end = e1["position"] self._exons_start = e0["position"] else: self._coding = (b0["position"] + b0["offset"], b1["position"] + b1["offset"] +1) - self._cds_len = abs((b1["position"] + b1["offset"]) - (b0["position"] + b0["offset"])) + self._cds_len = (b1["position"] + b1["offset"]) - (b0["position"] + b0["offset"]) self._exons_end = e1["position"] self._exons_start = e0["position"] @@ -202,7 +202,7 @@ def coordinate_to_protein(self, coordinate): :arg int coordinate: Coordinate. - :returns tuple: Protein position (p.). + :returns dict: Protein position model(p.). """ pos = self.coordinate_to_coding(coordinate) @@ -216,20 +216,20 @@ def coordinate_to_protein(self, coordinate): "position_in_codon": (pos["position"]+2) % 3 + 1, **{k: v for k, v in pos.items() if k != "position"}} - def protein_to_coordinate(self, position): + def protein_to_coordinate(self, pos_m): """Convert a protein position (p.) to a coordinate. - :arg tuple position: Protein position (p.). + :arg dict position: Protein position model(p.). :returns int: Coordinate. """ - if position["region"] in ["-", "*"]: + if pos_m["region"] in ["-", "*"]: return self.coding_to_coordinate( - {"position": 3 * position["position"] + position["position_in_codon"] - 3, - "offset": position["offset"], - "region": position["region"]}) + {"position": 3 * pos_m["position"] + pos_m["position_in_codon"] - 3, + "offset": pos_m["offset"], + "region": pos_m["region"]}) return self.coding_to_coordinate( - {"position": 3 * position["position"] + position["position_in_codon"] - 3, - "offset": position["offset"], - "region": position["region"]}) + {"position": 3 * pos_m["position"] + pos_m["position_in_codon"] - 3, + "offset": pos_m["offset"], + "region": pos_m["region"]}) From 5f2bef7a142ebe0d1bad23dcd437d861719dd09f Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 11 Mar 2026 11:55:38 +0100 Subject: [PATCH 055/236] Local copy before checkout --- tests/helper.py | 19 +++++++++++++++---- tests/test_crossmapper.py | 6 ++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/helper.py b/tests/helper.py index a17b119..9e51367 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -2,8 +2,19 @@ def invariant(f, x, f_i, y): assert f(x) == y assert f_i(y) == x - def degenerate_equal(f, coordinate, locations): - assert f(locations[0]) == coordinate - assert len( - set(map(f, locations))) == 1 + results = [f(loc) for loc in locations] + + # First condition: first maps correctly + assert results[0] == coordinate, ( + f"\nFirst location: {locations[0]}" + f"\nExpected: {coordinate}" + f"\nGot: {results[0]}" + ) + + # Second condition: all map to same coordinate + assert len(set(results)) == 1, ( + f"\nLocations: {locations}" + f"\nResults: {results}" + f"\nExpected all to map to the same coordinate" + ) \ No newline at end of file diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 788863b..d22715b 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -29,6 +29,12 @@ def test_NonCoding(): crossmap = NonCoding(_exons) # Boundary between upstream and transcript. + invariant( + crossmap.coordinate_to_noncoding, + 3, + crossmap.noncoding_to_coordinate, + {"position": 2, "offset": 0, "region": "u"}, + ) invariant( crossmap.coordinate_to_noncoding, 4, From 55fe7979a8f353a2cdee9d7792adda36619a099c Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:41:39 +0100 Subject: [PATCH 056/236] Update table for Genomic and NonCoding classes in document --- README.rst | 129 ++++++++++------------------------------------------- 1 file changed, 24 insertions(+), 105 deletions(-) diff --git a/README.rst b/README.rst index afeb981..6559526 100644 --- a/README.rst +++ b/README.rst @@ -60,17 +60,17 @@ The ``Genomic`` class provides an interface for conversions between genomic posi Genomic Position Model ~~~~~~~~~~~~~~~~~~~~~~~ -Genomic positions follow the HGVS ``g`` coordinate system. They are represented as dictionaries: +Genomic positions follow the HGVS ``g`` coordinate system. They are represented as dictionaries. Below is an example of `g.1` in HGVS. .. code-block:: json { - "position": int + "position": 1 } Where: -- **position**: a positive integer +- **position**: a positive integer(>0) Genomic Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -83,17 +83,6 @@ Genomic Position Conversion {"position": 1} >>> crossmap.genomic_to_coordinate({"position": 1}) 0 -Here is the mapping of coordinates to genomic positions: - -.. csv-table:: Coordinate to Genomic Position (0-4) - :header: "Coordinate", "Position" - - 0, 1 - 1, 2 - 2, 3 - 3, 4 - 4, 5 - ... NonCoding Class --------------- @@ -103,21 +92,21 @@ The ``NonCoding`` class provides conversions between noncoding positions and coo NonCoding Position Model ~~~~~~~~~~~~~~~~~~~~~~~ -Noncoding positions follow the HGVS ``n`` coordinate system. They are represented as dictionaries: +Noncoding positions follow the HGVS ``n`` coordinate system. They are represented as dictionaries. Below is an example of ``n.14+1`` in HGVS. .. code-block:: json { - "position": int, - "offset": int, - "region": str + "position": 10, + "offset": -5, + "region": '' } Where: -- **position**: a positive integer +- **position**: a positive integer (>0) - **offset**: an integer indicating the offset relative to the position (negative for upstream, positive for downstream) -- **region**: a string describing the region type (``""`` for standard, ``"u"`` for upstream, ``"d"`` for downstream) +- **region**: a string describing the region type (``''`` for standard, ``'u'`` for upstream, ``'d'`` for downstream) NonCoding Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -126,10 +115,10 @@ NonCoding Position Conversion >>> from mutalyzer_crossmapper import NonCoding >>> crossmap = NonCoding(_exons) - >>> crossmap.coordinate_to_noncoding(35) - {"position": 14, "offset": 1, "region": ""} - >>> crossmap.noncoding_to_coordinate({"position": 14, "offset": 1, "region": ""}) - 35 + >>> crossmap.coordinate_to_noncoding(25) + {"position": 10, "offset": -5, "region": ""} + >>> crossmap.noncoding_to_coordinate({"position": 10, "offset": -5, "region": ''}) + 25 Notes ~~~~~ @@ -140,88 +129,18 @@ Here is the mapping of coordinates to noncoding positions: .. csv-table:: :class: table-scroll - :header: "Coordinate", "Position", "Offset", "Region" + :header: "Coordinate", "Position", "Offset", "Region", "HGVS" - "0", "5","0", "u" - "1", "4","0", "u" - "2", "3","0", "u" - "3", "2","0", "u" - "4", "1","0", "u" - "5", "1","0", "" - "6", "2","0", "" - "7", "3","0", "" - "8", "3","1", "" - "9", "3","2", "" - "10", "3","3", "" - "11", "4","-3", "" - "12", "4","-2", "" - "13", "4","-1", "" - "14", "4","0", "" - "15", "5","0", "" - "16", "6","0", "" - "17", "7","0", "" - "18", "8","0", "" - "19", "9","0", "" - "20", "9","1", "" - "21", "9","2", "" - "22", "9","3", "" - "23", "9","4", "" - "24", "9","5", "" - "25", "10","-5", "" - "26", "10","-4", "" - "27", "10","-3", "" - "28", "10","-2", "" - "29", "10","-1", "" - "30", "10","0", "" - "31", "11","0", "" - "32", "12","0", "" - "33", "13","0", "" - "34", "14","0", "" - "35", "14","1", "" - "36", "14","2", "" - "37", "14","3", "" - "38", "15","-2", "" - "39", "15","-1", "" - "40", "15","0", "" - "41", "16","0", "" - "42", "17","0", "" - "43", "18","0", "" - "44", "18","1", "" - "45", "18","2", "" - "46", "18","3", "" - "47", "19","-3", "" - "48", "19","-2", "" - "49", "19","-1", "" - "50", "19","0", "" - "51", "20","0", "" - "52", "20","1", "" - "53", "20","2", "" - "54", "20","3", "" - "55", "20","4", "" - "56", "20","5", "" - "57", "20","6", "" - "58", "20","7", "" - "59", "20","8", "" - "60", "20","9", "" - "61", "21","-9", "" - "62", "21","-8", "" - "63", "21","-7", "" - "64", "21","-6", "" - "65", "21","-5", "" - "66", "21","-4", "" - "67", "21","-3", "" - "68", "21","-2", "" - "69", "21","-1", "" - "70", "21","0", "" - "71", "22","0", "" - "72", "1","0", "d" - "73", "2","0", "d" - "74", "3","0", "d" - "75", "4","0", "d" - "76", "5","0", "d" - "77", "6","0", "d" - "78", "7","0", "d" - "79", "8","0", "d" + "4", "1","0", "u", "n.u1" + "5", "1","0", "", "n.1" + ... + "24", "9","5", "", "n.9+5" + "25", "10","-5", "", "n.10-5" + ... + "71", "22","0", "", "n.22" + "72", "1","0", "d", "n.d1" + ... + "79", "8","0", "d", "n.d8" From a2003b362a95c9834d3a8c004483bf0344c0d403 Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:52:12 +0100 Subject: [PATCH 057/236] Update table for Coding class in document --- README.rst | 89 ++++++------------------------------------------------ 1 file changed, 10 insertions(+), 79 deletions(-) diff --git a/README.rst b/README.rst index 6559526..ece04cf 100644 --- a/README.rst +++ b/README.rst @@ -133,13 +133,10 @@ Here is the mapping of coordinates to noncoding positions: "4", "1","0", "u", "n.u1" "5", "1","0", "", "n.1" - ... "24", "9","5", "", "n.9+5" "25", "10","-5", "", "n.10-5" - ... "71", "22","0", "", "n.22" "72", "1","0", "d", "n.d1" - ... "79", "8","0", "d", "n.d8" @@ -152,14 +149,14 @@ The ``Coding`` class provides conversions between coding positions and coordinat Coding Position Model ~~~~~~~~~~~~~~~~~~~~ -Coding positions follow the HGVS ``c`` coordinate system. They are represented as dictionaries: +Coding positions follow the HGVS ``c`` coordinate system. They are represented as dictionaries. Here is an example of ``c.*1+3``. .. code-block:: json { - "position": int, - "offset": int, - "region": str + "position": 1, + "offset": 3, + "region": '*' } Where: @@ -175,10 +172,10 @@ Coding Position Conversion >>> from mutalyzer_crossmapper import Coding >>> crossmap = Coding(_exons, _cds) - >>> crossmap.coordinate_to_coding(31) - {"position": -1, "offset": 0, "region": '-'} - >>> crossmap.coding_to_coordinate({"position": -1, "offset": 0, "region": '-'}) - 31 + >>> crossmap.coordinate_to_coding(46) + {"position": 1, "offset": 3, "region": '*'} + >>> crossmap.coding_to_coordinate({"position": 1, "offset": 3, "region": '*'}) + 46 Notes ~~~~~ @@ -189,88 +186,22 @@ Here is the mapping of coordinates to coding positions: .. csv-table:: :class: table-scroll - :header: "Coordinate", "Position", "Offset", "Region" + :header: "Coordinate", "Position", "Offset", "Region", "HGVS" - "0", "5","0", "u" - "1", "4","0", "u" - "2", "3","0", "u" - "3", "2","0", "u" "4", "1","0", "u" "5", "11","0", '-' - "6", "10","0", '-' - "7", "9","0", '-' - "8", "9","1", '-' - "9", "9","2", '-' - "10", "9","3", '-' - "11", "8","-3", '-' - "12", "8","-2", '-' - "13", "8","-1", '-' - "14", "8","0", '-' - "15", "7","0", '-' - "16", "6","0", '-' - "17", "5","0", '-' - "18", "4","0", '-' - "19", "3","0", '-' - "20", "3","1", '-' - "21", "3","2", '-' - "22", "3","3", '-' - "23", "3","4", '-' "24", "3","5", '-' "25", "2","-5", '-' - "26", "2","-4", '-' - "27", "2","-3", '-' - "28", "2","-2", '-' - "29", "2","-1", '-' - "30", "2","0", '-' "31", "1","0", '-' "32", "1","0", "" - "33", "2","0", "" - "34", "3","0", "" - "35", "3","1", "" - "36", "3","2", "" "37", "3","3", "" "38", "4","-2", "" - "39", "4","-1", "" - "40", "4","0", "" - "41", "5","0", "" - "42", "6","0", "" "43", "1","0", '*' - "44", "1","1", '*' - "45", "1","2", '*' - "46", "1","3", '*' - "47", "2","-3", '*' - "48", "2","-2", '*' - "49", "2","-1", '*' - "50", "2","0", '*' - "51", "3","0", '*' - "52", "3","1", '*' - "53", "3","2", '*' - "54", "3","3", '*' - "55", "3","4", '*' - "56", "3","5", '*' - "57", "3","6", '*' - "58", "3","7", '*' - "59", "3","8", '*' "60", "3","9", '*' "61", "4","-9", '*' - "62", "4","-8", '*' - "63", "4","-7", '*' - "64", "4","-6", '*' - "65", "4","-5", '*' - "66", "4","-4", '*' - "67", "4","-3", '*' - "68", "4","-2", '*' - "69", "4","-1", '*' - "70", "4","0", '*' "71", "5","0", '*' "72", "1","0", "d" - "73", "2","0", "d" - "74", "3","0", "d" - "75", "4","0", "d" - "76", "5","0", "d" - "77", "6","0", "d" - "78", "7","0", "d" - "79", "8","0", "d" + From 4d9089a500e500e8a52c6194490ac72350c61051 Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:58:54 +0100 Subject: [PATCH 058/236] Add HGVS column in coding table --- README.rst | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index ece04cf..f316b9f 100644 --- a/README.rst +++ b/README.rst @@ -188,19 +188,19 @@ Here is the mapping of coordinates to coding positions: :class: table-scroll :header: "Coordinate", "Position", "Offset", "Region", "HGVS" - "4", "1","0", "u" - "5", "11","0", '-' - "24", "3","5", '-' - "25", "2","-5", '-' - "31", "1","0", '-' - "32", "1","0", "" - "37", "3","3", "" - "38", "4","-2", "" - "43", "1","0", '*' - "60", "3","9", '*' - "61", "4","-9", '*' - "71", "5","0", '*' - "72", "1","0", "d" + "4", "1","0", "u", "c.u1" + "5", "11","0", '-', "c.-11" + "24", "3","5", '-', "c.-3+5" + "25", "2","-5", '-', "c.-2-5" + "31", "1","0", '-', "c.-1" + "32", "1","0", "", "c.1" + "37", "3","3", "", "c.3+3" + "38", "4","-2", "", "c.4-2" + "43", "1","0", '*', "c.*1" + "60", "3","9", '*', "c.*3+9" + "61", "4","-9", '*', "c.*4+9" + "71", "5","0", '*', "c.*5" + "79", "8","0", "d", "c.d8" From 15428949bea49d2dcab35cefa27b0a20aa680a72 Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:06:51 +0100 Subject: [PATCH 059/236] Fix sytax --- README.rst | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index f316b9f..86ccdb0 100644 --- a/README.rst +++ b/README.rst @@ -116,7 +116,7 @@ NonCoding Position Conversion >>> from mutalyzer_crossmapper import NonCoding >>> crossmap = NonCoding(_exons) >>> crossmap.coordinate_to_noncoding(25) - {"position": 10, "offset": -5, "region": ""} + {"position": 10, "offset": -5, "region": ''} >>> crossmap.noncoding_to_coordinate({"position": 10, "offset": -5, "region": ''}) 25 @@ -131,13 +131,13 @@ Here is the mapping of coordinates to noncoding positions: :class: table-scroll :header: "Coordinate", "Position", "Offset", "Region", "HGVS" - "4", "1","0", "u", "n.u1" - "5", "1","0", "", "n.1" - "24", "9","5", "", "n.9+5" - "25", "10","-5", "", "n.10-5" - "71", "22","0", "", "n.22" - "72", "1","0", "d", "n.d1" - "79", "8","0", "d", "n.d8" + "4", "1","0", "'u'", "n.u1" + "5", "1","0", "''", "n.1" + "24", "9","5", "''", "n.9+5" + "25", "10","-5", "''", "n.10-5" + "71", "22","0", "''", "n.22" + "72", "1","0", "'d'", "n.d1" + "79", "8","0", "'d'", "n.d8" @@ -188,19 +188,19 @@ Here is the mapping of coordinates to coding positions: :class: table-scroll :header: "Coordinate", "Position", "Offset", "Region", "HGVS" - "4", "1","0", "u", "c.u1" + "4", "1","0", "'u'", "c.u1" "5", "11","0", '-', "c.-11" "24", "3","5", '-', "c.-3+5" "25", "2","-5", '-', "c.-2-5" "31", "1","0", '-', "c.-1" - "32", "1","0", "", "c.1" - "37", "3","3", "", "c.3+3" - "38", "4","-2", "", "c.4-2" + "32", "1","0", "''", "c.1" + "37", "3","3", "''", "c.3+3" + "38", "4","-2", "''", "c.4-2" "43", "1","0", '*', "c.*1" "60", "3","9", '*', "c.*3+9" "61", "4","-9", '*', "c.*4+9" "71", "5","0", '*', "c.*5" - "79", "8","0", "d", "c.d8" + "79", "8","0", "'d'", "c.d8" From 75a5cba52247e006eb42e013d1c66427bc8059de Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:14:22 +0100 Subject: [PATCH 060/236] Update protein example in document --- README.rst | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 86ccdb0..b1749f1 100644 --- a/README.rst +++ b/README.rst @@ -64,9 +64,7 @@ Genomic positions follow the HGVS ``g`` coordinate system. They are represented .. code-block:: json - { - "position": 1 - } + {"position": 1} Where: @@ -211,15 +209,15 @@ Protein Protein Position Model ~~~~~~~~~~~~~~~~~~~~~~ -Protein positions follow the HGVS ``p`` coordinate system. They are represented as dictionaries: +Protein positions follow the HGVS ``p`` coordinate system. They are represented as dictionaries. Here is an example of ``p.1`` in HGVS. .. code-block:: json { - "position": int, - "position_in_codon": int, - "offset": int, - "region": str + "position": 1, + "position_in_codon": 3, + "offset": 3, + "region": '' } Where: @@ -227,7 +225,7 @@ Where: - **position**: the amino acid position (1-based) - **position_in_codon**: the codon nucleotide index (1, 2, or 3) - **offset**: an integer indicating offset relative to the codon -- **region**: a string describing the region type (``""`` for standard positions) +- **region**: a string describing the region type (``''`` for standard positions) Protein Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -236,10 +234,10 @@ Conversions between protein positions and coordinates: .. code-block:: python - >>> crossmap.coordinate_to_protein(41) - {"position": 2, "position_in_codon": 2, "offset": 1, "region": ""} - >>> crossmap.protein_to_coordinate({"position": 2, "position_in_codon": 2, "offset": 1, "region": ""}) - 41 + >>> crossmap.coordinate_to_protein(37) + {"position": 1, "position_in_codon": 3, "offset": 3, "region": ""} + >>> crossmap.protein_to_coordinate({"position": 1, "position_in_codon": 3, "offset": 3, "region": ""}) + 37 .. _numbering: http://varnomen.hgvs.org/bg-material/numbering/ From b0ed66feeceab9cf802f59d7960a5c4253f18690 Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:14:42 +0100 Subject: [PATCH 061/236] Add protein table in document --- README.rst | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index b1749f1..a31322f 100644 --- a/README.rst +++ b/README.rst @@ -97,7 +97,7 @@ Noncoding positions follow the HGVS ``n`` coordinate system. They are represente { "position": 10, "offset": -5, - "region": '' + "region": "" } Where: @@ -217,7 +217,7 @@ Protein positions follow the HGVS ``p`` coordinate system. They are represented "position": 1, "position_in_codon": 3, "offset": 3, - "region": '' + "region": "" } Where: @@ -240,5 +240,26 @@ Conversions between protein positions and coordinates: 37 +Here is the mapping of coordinates to coding positions: + +.. csv-table:: + :class: table-scroll + :header: "Coordinate", "Position", "position_in_codon", "Offset", "Region", "HGVS" + + "0", "4", "2", "0", "'u'", + "4", "4", "2", "0", "'u'", + "5", "4", "2", "0", '-', + "6", "4", "3", "0", '-', + "7", "3", "1", "0", '-', + "31", "1", "3", "0", '-', + "32", "1", "1", "0", "''", "p.1" + "42", "2", "3", "0", "''", "p.2" + "43", "1", "1", "0", '*', + "44", "1", "1", "1", '*', + "79", "2", "2", "0", "'d'", + + + + .. _numbering: http://varnomen.hgvs.org/bg-material/numbering/ .. _ReadTheDocs: https://mutalyzer-crossmapper.readthedocs.io From 16e9a62261606689df9f3336d6f22bd3ed59d680 Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:15:33 +0100 Subject: [PATCH 062/236] Fix typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a31322f..29f15d1 100644 --- a/README.rst +++ b/README.rst @@ -240,7 +240,7 @@ Conversions between protein positions and coordinates: 37 -Here is the mapping of coordinates to coding positions: +Here is the mapping of coordinates to protein positions: .. csv-table:: :class: table-scroll From d6c868d168ed855ed98eb755c69285a7b95ee2e1 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 13 Mar 2026 09:17:35 +0100 Subject: [PATCH 063/236] Add tests --- tests/test_crossmapper.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index d22715b..40546b8 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -107,6 +107,7 @@ def test_NonCoding_degenerate(): [ {"position": 1, "offset": 0, "region": "u"}, {"position": 0, "offset": -1, "region": "u"}, + {"position": 1, "offset": -1, "region": ""}, ], ) @@ -117,6 +118,7 @@ def test_NonCoding_degenerate(): [ {"position": 1, "offset": 0, "region": "d"}, {"position": 0, "offset": 1, "region": "d"}, + {"position": 22, "offset": 1, "region": ""}, ], ) @@ -129,14 +131,18 @@ def test_NonCoding_inverted_degenerate(): degenerate_equal( crossmap.noncoding_to_coordinate, 72, - [{"position": 1, "offset": 0, "region": "u"}], + [ + {"position": 1, "offset": 0, "region": "u"}, + {"position": 1, "offset": -1, "region": ""},], ) # Boundary between downstream and transcript. degenerate_equal( crossmap.noncoding_to_coordinate, 4, - [{"position": 1, "offset": 0, "region": "d"}], + [ + {"position": 1, "offset": 0, "region": "d"}, + {"position": 22, "offset": 1, "region": ""},], ) @@ -459,6 +465,8 @@ def test_Coding_degenerate(): {"position": 1, "offset": 0, "region": "u"}, {"position": 2, "offset": 1, "region": "u"}, {"position": 0, "offset": -1, "region": "u"}, + {"position": 1, "offset": -1, "region": "-"}, + {"position": 1, "offset": -2, "region": ""}, ], ) degenerate_equal( @@ -468,6 +476,9 @@ def test_Coding_degenerate(): {"position": 1, "offset": 0, "region": "d"}, {"position": 8, "offset": -7, "region": "d"}, {"position": 0, "offset": -1, "region": "d"}, + {"position": 8, "offset": 2, "region": ""}, + {"position": 2, "offset": 0, "region": "*"}, + {"position": 1, "offset": 1, "region": "*"}, ], ) @@ -483,7 +494,10 @@ def test_Coding_inverted_degenerate(): {"position": 1, "offset": 0, "region": "u"}, {"position": 2, "offset": 1, "region": "u"}, {"position": 0, "offset": -1, "region": "u"}, + {"position": 1, "offset": -2, "region": ""}, + {"position": 2, "offset": -3, "region": ""}, {"position": 2, "offset": 0, "region": "-"}, + {"position": 1, "offset": -1, "region": "-"}, ], ) degenerate_equal( @@ -492,6 +506,8 @@ def test_Coding_inverted_degenerate(): [ {"position": 1, "offset": 0, "region": "d"}, {"position": 2, "offset": -1, "region": "d"}, + {"position": 8, "offset": 2, "region": ""}, + {"position": 7, "offset": 3, "region": ""}, {"position": 2, "offset": 0, "region": "*"}, ], ) @@ -555,6 +571,9 @@ def test_Coding_no_utr_degenerate(): {"position": 2, "offset": 1, "region": "u"}, {"position": 1, "offset": 0, "region": "u"}, {"position": 1, "offset": 0, "region": "-"}, + {"position": 2, "offset": 1, "region": "-"}, + {"position": 1, "offset": -1, "region": ""}, + {"position": 2, "offset": -2, "region": ""}, ], ) degenerate_equal( @@ -564,6 +583,9 @@ def test_Coding_no_utr_degenerate(): {"position": 1, "offset": 0, "region": "d"}, {"position": 2, "offset": -1, "region": "d"}, {"position": 1, "offset": 0, "region": "*"}, + {"position": 2, "offset": -1, "region": "*"}, + {"position": 3, "offset": -2, "region": "*"}, + {"position": 1, "offset": 1, "region": ""}, ], ) @@ -579,7 +601,8 @@ def test_Coding_inverted_no_utr_degenerate(): {"position": 1, "offset": 0, "region": "u"}, {"position": 2, "offset": 1, "region": "u"}, {"position": 1, "offset": 0, "region": "-"}, - + {"position": 2, "offset": 1, "region": "-"}, + {"position": 1, "offset": -1, "region": ""}, ], ) degenerate_equal( @@ -645,7 +668,7 @@ def test_Coding_protein(): crossmap.coordinate_to_protein, 31, crossmap.protein_to_coordinate, - {"position": 1, "position_in_codon": 1, "offset": 0, "region": "-"}, + {"position": 1, "position_in_codon": 3, "offset": 0, "region": "-"}, ) invariant( crossmap.coordinate_to_protein, From 41bd3d5939a758ca057c7f941a1d7ad72236f23d Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 13 Mar 2026 09:26:18 +0100 Subject: [PATCH 064/236] Refactor crossmap --- mutalyzer_crossmapper/crossmapper.py | 50 ++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index c16fdf7..a714902 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -75,12 +75,12 @@ def __init__(self, locations, cds, inverted=False): if self._inverted: self._coding = (b1["position"] + b1["offset"], b0["position"] + b0["offset"] + 1) self._cds_len = (b0["position"] + b0["offset"]) - (b1["position"] + b1["offset"]) - self._exons_end = e1["position"] + self._exons = (e1["position"], e0["position"]) self._exons_start = e0["position"] else: self._coding = (b0["position"] + b0["offset"], b1["position"] + b1["offset"] +1) self._cds_len = (b1["position"] + b1["offset"]) - (b0["position"] + b0["offset"]) - self._exons_end = e1["position"] + self._exons = (e0["position"], e1["position"]) self._exons_start = e0["position"] def _coordinate_to_coding(self, coordinate): @@ -126,7 +126,7 @@ def coordinate_to_coding(self, coordinate, degenerate=False): if degenerate: if pos_m["region"] == "u": if self._inverted: - pos_m["position"] = pos_m["position"] + self._exons_start - self._coding[1] + 1 + pos_m["position"] = pos_m["position"] + self._exons[1] - self._coding[1] + 1 else: pos_m["position"] = pos_m["position"] + self._coding[0] pos_m["region"] = "-" @@ -134,7 +134,7 @@ def coordinate_to_coding(self, coordinate, degenerate=False): if self._inverted: pos_m["position"] = pos_m["position"] + self._coding[0] else: - pos_m["position"] = pos_m["position"] + self._exons_end - self._coding[1] + 1 + pos_m["position"] = pos_m["position"] + self._exons[1]- self._coding[1] + 1 pos_m["region"] = "*" return pos_m @@ -171,8 +171,8 @@ def coding_to_coordinate(self, pos_m): elif region == "-": if position > self._coding[0]: noncoding_pos_m = { - "position": position - self._coding[0], - "offset": offset, + "position": position - self._coding[0] - offset, + "offset": 0, "region": "u" } else: @@ -182,10 +182,10 @@ def coding_to_coordinate(self, pos_m): "region": "" } else: # * - if position > self._coding[1]: + if position > self._coding[0]: noncoding_pos_m = { - "position": position - self._coding[1], - "offset": offset, + "position": position - self._coding[0] + offset, + "offset": 0, "region": "d" } else: @@ -206,10 +206,30 @@ def coordinate_to_protein(self, coordinate): """ pos = self.coordinate_to_coding(coordinate) - if pos["region"] in ["-", "*"]: + if pos["region"] == "u": + pos = self.coordinate_to_coding(coordinate + pos["position"]) return { "position": pos["position"] // 3 + 1, "position_in_codon": pos["position"] % 3, + "region": "u", + **{k: v for k, v in pos.items() if k not in ["position", "region"]}} + elif pos["region"] == "d": + pos = self.coordinate_to_coding(coordinate - pos["position"]) + return { + "position": pos["position"] // 3 + 1, + "position_in_codon": pos["position"] % 3, + "region": "d", + **{k: v for k, v in pos.items() if k not in ["position", "region"]}} + + if pos["region"] == "-": + return { + "position": (pos["position"]+2) // 3, + "position_in_codon": -pos["position"] % 3 + 1, + **{k: v for k, v in pos.items() if k != "position"}} + if pos["region"] == "*": + return { + "position": (pos["position"]+2)// 3, + "position_in_codon": (pos["position"] + 2) % 3 + 1, **{k: v for k, v in pos.items() if k != "position"}} return { "position": (pos["position"]+2) // 3, @@ -223,12 +243,16 @@ def protein_to_coordinate(self, pos_m): :returns int: Coordinate. """ - if pos_m["region"] in ["-", "*"]: + if pos_m["region"] == "-": return self.coding_to_coordinate( - {"position": 3 * pos_m["position"] + pos_m["position_in_codon"] - 3, + {"position": 3 * pos_m["position"] - pos_m["position_in_codon"] + 1, "offset": pos_m["offset"], "region": pos_m["region"]}) - + if pos_m["region"] == "*": + return self.coding_to_coordinate( + {"position": 3 * pos_m["position"] + pos_m["position_in_codon"] - 3, + "offset": pos_m["offset"], + "region": pos_m["region"]}) return self.coding_to_coordinate( {"position": 3 * pos_m["position"] + pos_m["position_in_codon"] - 3, "offset": pos_m["offset"], From 17523633f4aa100272199aa37f49aedc6af94a21 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 13 Mar 2026 09:44:30 +0100 Subject: [PATCH 065/236] Cleanup --- mutalyzer_crossmapper/crossmapper.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index a714902..dfdb23d 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -92,6 +92,7 @@ def _coordinate_to_coding(self, coordinate): """ noncoding_pos_m = self._noncoding.to_position(coordinate) location = noncoding_pos_m["position"] + if noncoding_pos_m["region"] == "": if location < self._coding[0]: return { @@ -123,18 +124,21 @@ def coordinate_to_coding(self, coordinate, degenerate=False): :returns dict: Coding position model (c./r.). """ pos_m = self._coordinate_to_coding(coordinate) + region = pos_m["region"] + position = pos_m["position"] + if degenerate: - if pos_m["region"] == "u": + if region == "u": if self._inverted: - pos_m["position"] = pos_m["position"] + self._exons[1] - self._coding[1] + 1 + pos_m["position"] = position + self._exons[1] - self._coding[1] + 1 else: - pos_m["position"] = pos_m["position"] + self._coding[0] + pos_m["position"] = position + self._coding[0] pos_m["region"] = "-" - if pos_m["region"] == "d": + if region == "d": if self._inverted: - pos_m["position"] = pos_m["position"] + self._coding[0] + pos_m["position"] = position + self._coding[0] else: - pos_m["position"] = pos_m["position"] + self._exons[1]- self._coding[1] + 1 + pos_m["position"] = position + self._exons[1]- self._coding[1] + 1 pos_m["region"] = "*" return pos_m @@ -167,9 +171,8 @@ def coding_to_coordinate(self, pos_m): "offset": offset, "region": "" } - # add checks for degenerate results? elif region == "-": - if position > self._coding[0]: + if position > self._coding[0]: # correct it to 'u' noncoding_pos_m = { "position": position - self._coding[0] - offset, "offset": 0, @@ -182,7 +185,7 @@ def coding_to_coordinate(self, pos_m): "region": "" } else: # * - if position > self._coding[0]: + if position > self._coding[0]: # correct it to 'd' noncoding_pos_m = { "position": position - self._coding[0] + offset, "offset": 0, @@ -213,7 +216,8 @@ def coordinate_to_protein(self, coordinate): "position_in_codon": pos["position"] % 3, "region": "u", **{k: v for k, v in pos.items() if k not in ["position", "region"]}} - elif pos["region"] == "d": + + if pos["region"] == "d": pos = self.coordinate_to_coding(coordinate - pos["position"]) return { "position": pos["position"] // 3 + 1, @@ -226,11 +230,13 @@ def coordinate_to_protein(self, coordinate): "position": (pos["position"]+2) // 3, "position_in_codon": -pos["position"] % 3 + 1, **{k: v for k, v in pos.items() if k != "position"}} + if pos["region"] == "*": return { "position": (pos["position"]+2)// 3, "position_in_codon": (pos["position"] + 2) % 3 + 1, **{k: v for k, v in pos.items() if k != "position"}} + return { "position": (pos["position"]+2) // 3, "position_in_codon": (pos["position"]+2) % 3 + 1, From 31f483adcf45b41823693aa4c5adad8fdcb8f668 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 13 Mar 2026 09:46:06 +0100 Subject: [PATCH 066/236] Cleanup --- mutalyzer_crossmapper/crossmapper.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index dfdb23d..cf751da 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -76,12 +76,10 @@ def __init__(self, locations, cds, inverted=False): self._coding = (b1["position"] + b1["offset"], b0["position"] + b0["offset"] + 1) self._cds_len = (b0["position"] + b0["offset"]) - (b1["position"] + b1["offset"]) self._exons = (e1["position"], e0["position"]) - self._exons_start = e0["position"] else: self._coding = (b0["position"] + b0["offset"], b1["position"] + b1["offset"] +1) self._cds_len = (b1["position"] + b1["offset"]) - (b0["position"] + b0["offset"]) self._exons = (e0["position"], e1["position"]) - self._exons_start = e0["position"] def _coordinate_to_coding(self, coordinate): """Convert a coordinate to a coding position (c./r.). From eeacf85fac8dab54e1cf9bcf4d55eafb46c46e9a Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 13 Mar 2026 09:47:34 +0100 Subject: [PATCH 067/236] Cleanup --- mutalyzer_crossmapper/crossmapper.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index cf751da..d1a4cec 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -104,12 +104,11 @@ def _coordinate_to_coding(self, coordinate): "offset": noncoding_pos_m["offset"], "region": "*" } - else: - return { - "position": location - self._coding[0] + 1, - "offset": noncoding_pos_m["offset"], - "region": "" - } + return { + "position": location - self._coding[0] + 1, + "offset": noncoding_pos_m["offset"], + "region": "" + } else: return noncoding_pos_m From bb8ee668620aa8be644ad412a10031d52d9493e6 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 13 Mar 2026 09:50:57 +0100 Subject: [PATCH 068/236] Cleanup --- mutalyzer_crossmapper/crossmapper.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index d1a4cec..31de6c6 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -251,11 +251,6 @@ def protein_to_coordinate(self, pos_m): {"position": 3 * pos_m["position"] - pos_m["position_in_codon"] + 1, "offset": pos_m["offset"], "region": pos_m["region"]}) - if pos_m["region"] == "*": - return self.coding_to_coordinate( - {"position": 3 * pos_m["position"] + pos_m["position_in_codon"] - 3, - "offset": pos_m["offset"], - "region": pos_m["region"]}) return self.coding_to_coordinate( {"position": 3 * pos_m["position"] + pos_m["position_in_codon"] - 3, "offset": pos_m["offset"], From 87f507d0b8916c5764d448ad467d74dd89d82fda Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 13 Mar 2026 11:38:43 +0100 Subject: [PATCH 069/236] Add missing tests --- tests/test_crossmapper.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 40546b8..00caa14 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -106,7 +106,6 @@ def test_NonCoding_degenerate(): 4, [ {"position": 1, "offset": 0, "region": "u"}, - {"position": 0, "offset": -1, "region": "u"}, {"position": 1, "offset": -1, "region": ""}, ], ) @@ -117,8 +116,7 @@ def test_NonCoding_degenerate(): 72, [ {"position": 1, "offset": 0, "region": "d"}, - {"position": 0, "offset": 1, "region": "d"}, - {"position": 22, "offset": 1, "region": ""}, + {"position": 23, "offset": 0, "region": ""}, ], ) @@ -132,8 +130,8 @@ def test_NonCoding_inverted_degenerate(): crossmap.noncoding_to_coordinate, 72, [ - {"position": 1, "offset": 0, "region": "u"}, - {"position": 1, "offset": -1, "region": ""},], + {"position": 1, "offset": -1, "region": ""}, + {"position": 1, "offset": 0, "region": "u"},], ) # Boundary between downstream and transcript. @@ -463,22 +461,19 @@ def test_Coding_degenerate(): 9, [ {"position": 1, "offset": 0, "region": "u"}, - {"position": 2, "offset": 1, "region": "u"}, - {"position": 0, "offset": -1, "region": "u"}, - {"position": 1, "offset": -1, "region": "-"}, + {"position": 2, "offset": 0, "region": "-"}, {"position": 1, "offset": -2, "region": ""}, + {"position": 1, "offset": -10, "region": "*"}, ], ) degenerate_equal( crossmap.coding_to_coordinate, 20, [ - {"position": 1, "offset": 0, "region": "d"}, - {"position": 8, "offset": -7, "region": "d"}, - {"position": 0, "offset": -1, "region": "d"}, - {"position": 8, "offset": 2, "region": ""}, - {"position": 2, "offset": 0, "region": "*"}, {"position": 1, "offset": 1, "region": "*"}, + {"position": 2, "offset": 0, "region": "*"}, + {"position": 8, "offset": 2, "region": ""}, + {"position": 1, "offset": 10, "region": "-"}, ], ) From e5d734252aa7fef41bd3eebf6f60f2187653abeb Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 13 Mar 2026 13:18:51 +0100 Subject: [PATCH 070/236] Add missing tests --- tests/test_crossmapper.py | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 00caa14..55a9a92 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -487,12 +487,9 @@ def test_Coding_inverted_degenerate(): 20, [ {"position": 1, "offset": 0, "region": "u"}, - {"position": 2, "offset": 1, "region": "u"}, - {"position": 0, "offset": -1, "region": "u"}, - {"position": 1, "offset": -2, "region": ""}, - {"position": 2, "offset": -3, "region": ""}, {"position": 2, "offset": 0, "region": "-"}, - {"position": 1, "offset": -1, "region": "-"}, + {"position": 1, "offset": -2, "region": ""}, + {"position": 1, "offset": -10, "region": "*"}, ], ) degenerate_equal( @@ -500,10 +497,10 @@ def test_Coding_inverted_degenerate(): 9, [ {"position": 1, "offset": 0, "region": "d"}, - {"position": 2, "offset": -1, "region": "d"}, - {"position": 8, "offset": 2, "region": ""}, - {"position": 7, "offset": 3, "region": ""}, {"position": 2, "offset": 0, "region": "*"}, + {"position": 8, "offset": 2, "region": ""}, + {"position": 1, "offset": 10, "region": "-"}, + ], ) @@ -563,12 +560,9 @@ def test_Coding_no_utr_degenerate(): crossmap.coding_to_coordinate, 9, [ - {"position": 2, "offset": 1, "region": "u"}, {"position": 1, "offset": 0, "region": "u"}, {"position": 1, "offset": 0, "region": "-"}, - {"position": 2, "offset": 1, "region": "-"}, - {"position": 1, "offset": -1, "region": ""}, - {"position": 2, "offset": -2, "region": ""}, + {"position": 1, "offset": -2, "region": "*"}, ], ) degenerate_equal( @@ -576,11 +570,8 @@ def test_Coding_no_utr_degenerate(): 11, [ {"position": 1, "offset": 0, "region": "d"}, - {"position": 2, "offset": -1, "region": "d"}, {"position": 1, "offset": 0, "region": "*"}, - {"position": 2, "offset": -1, "region": "*"}, - {"position": 3, "offset": -2, "region": "*"}, - {"position": 1, "offset": 1, "region": ""}, + {"position": 1, "offset": 2, "region": "-"}, ], ) @@ -594,10 +585,8 @@ def test_Coding_inverted_no_utr_degenerate(): 11, [ {"position": 1, "offset": 0, "region": "u"}, - {"position": 2, "offset": 1, "region": "u"}, {"position": 1, "offset": 0, "region": "-"}, - {"position": 2, "offset": 1, "region": "-"}, - {"position": 1, "offset": -1, "region": ""}, + {"position": 1, "offset": -2, "region": "*"}, ], ) degenerate_equal( @@ -605,9 +594,8 @@ def test_Coding_inverted_no_utr_degenerate(): 9, [ {"position": 1, "offset": 0, "region": "d"}, - {"position": 2, "offset": -1, "region": "d"}, {"position": 1, "offset": 0, "region": "*"}, - {"position": 1, "offset": 1, "region": ""}, + {"position": 1, "offset": 2, "region": "-"}, ], ) From 53954d3bc26c4ec6feccd61d9504901ff45fc6ce Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 13 Mar 2026 13:21:48 +0100 Subject: [PATCH 071/236] Refactor --- mutalyzer_crossmapper/crossmapper.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 31de6c6..a334dd3 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -150,18 +150,8 @@ def coding_to_coordinate(self, pos_m): position = pos_m["position"] offset = pos_m["offset"] - if region == "u": - noncoding_pos_m = { - "position": position - offset, - "offset": 0, - "region": "u" - } - elif region == "d": - noncoding_pos_m = { - "position": position + offset, - "offset": 0, - "region": "d" - } + if region in ["u", "d"]: + return self._noncoding.to_coordinate(pos_m) elif region == "": noncoding_pos_m = { "position": position + self._coding[0] -1, From 6cb7d3238e83b7285403d71c820c966d055498d5 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 20 Mar 2026 09:34:25 +0100 Subject: [PATCH 072/236] Cleanup --- mutalyzer_crossmapper/multi_locus.py | 32 +++++++--------------------- tests/test_multi_locus.py | 1 - 2 files changed, 8 insertions(+), 25 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 2853ee6..861e5cd 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -68,24 +68,10 @@ def to_position(self, coordinate:int): "region": region } - if location["offset"] == 0: - return { - "position": location["position"] + self._offsets[self._direction(index)], - "offset": 0, - "region": "" - } - - if location["offset"] < 0: - return { - "position": self._offsets[self._direction(index)], - "offset": location["offset"], - "region": "" - } - - return{ + return { "position": location["position"] + self._offsets[self._direction(index)], "offset": location["offset"], - "region": "" + "region": region } def to_coordinate(self, pos_m:dict): @@ -97,13 +83,11 @@ def to_coordinate(self, pos_m:dict): """ region = pos_m["region"] - if region == "u": - if self._inverted: - return abs(pos_m["position"]) + self._locations[-1][1] + pos_m["offset"] - 1 - return self._locations[0][0] - abs(pos_m["position"]) + pos_m["offset"] - - if region == "d": + if pos_m["region"] in ("u", "d"): + is_upstream = region == "u" if self._inverted: + is_upstream = not is_upstream + if is_upstream: return self._locations[0][0] - abs(pos_m["position"]) + pos_m["offset"] return abs(pos_m["position"]) + self._locations[-1][1] + pos_m["offset"] - 1 @@ -111,5 +95,5 @@ def to_coordinate(self, pos_m:dict): len(self._offsets), max(0, bisect_right(self._offsets, pos_m["position"]) - 1) ) - pos_m["position"] = pos_m["position"] - self._offsets[index] - return self._loci[self._direction(index)].to_coordinate(pos_m) + locus_pos_m = {**pos_m, "position": pos_m["position"] - self._offsets[index]} + return self._loci[self._direction(index)].to_coordinate(locus_pos_m) diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index c08ff7f..3434a62 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -321,6 +321,5 @@ def test_MultiLocus_inverted_degenerate(): [ {"position": 0, "offset": -1, "region": "d"}, {"position": 1, "offset": 0, "region": "d"}, - {"position": 2, "offset": 1, "region": "d"}, ], ) From 3e32bb96e258a0e6f02f5886bdebc5579f173300 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 20 Mar 2026 09:35:05 +0100 Subject: [PATCH 073/236] Cleanup and add tests --- mutalyzer_crossmapper/crossmapper.py | 189 +++++++++++++-------------- tests/test_crossmapper.py | 29 +++- 2 files changed, 119 insertions(+), 99 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index a334dd3..0fd0c09 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -52,9 +52,10 @@ def noncoding_to_coordinate(self, pos_m): :returns int: Coordinate. """ + multilocus_pos_m = {**pos_m} if pos_m["region"] == "": - pos_m["position"] = pos_m["position"] - 1 - return self._noncoding.to_coordinate(pos_m) + multilocus_pos_m["position"] = pos_m["position"] - 1 + return self._noncoding.to_coordinate(multilocus_pos_m) class Coding(NonCoding): @@ -74,13 +75,52 @@ def __init__(self, locations, cds, inverted=False): if self._inverted: self._coding = (b1["position"] + b1["offset"], b0["position"] + b0["offset"] + 1) - self._cds_len = (b0["position"] + b0["offset"]) - (b1["position"] + b1["offset"]) self._exons = (e1["position"], e0["position"]) else: self._coding = (b0["position"] + b0["offset"], b1["position"] + b1["offset"] +1) - self._cds_len = (b1["position"] + b1["offset"]) - (b0["position"] + b0["offset"]) self._exons = (e0["position"], e1["position"]) + def _degenerate_position(self, pos_m): + """Degenerate a coding position model (c./r.). + + :arg dict pos_m: Coding position model. + + :returns dict: a generate coding position model. + """ + region = pos_m["region"] + position = pos_m["position"] + + degenerated_pos_m = {"offset": pos_m["offset"]} + + if region == "u": + if self._inverted: + degenerated_pos_m["position"] = position + self._exons[1] - self._coding[1] + 1 + else: + degenerated_pos_m["position"] = position + self._coding[0] + degenerated_pos_m["region"] = "-" + if region == "d": + if self._inverted: + degenerated_pos_m["position"] = position + self._coding[0] + else: + degenerated_pos_m["position"] = position + self._exons[1]- self._coding[1] + 1 + degenerated_pos_m["region"] = "*" + return degenerated_pos_m + + def _normalize_position(self, pos_m): + """Normalize a coding position model (c./r.). + + :arg dict pos_m: Coding position model. + + :returns dict: a normalized coding postion model. + """ + initial_pos = {**pos_m, "offset": 0} + coordinate = self._coding_to_coordinate(initial_pos) + if self._inverted: + coordinate = coordinate - pos_m["offset"] + else: + coordinate = coordinate + pos_m["offset"] + return self.coordinate_to_coding(coordinate) + def _coordinate_to_coding(self, coordinate): """Convert a coordinate to a coding position (c./r.). @@ -89,28 +129,29 @@ def _coordinate_to_coding(self, coordinate): :returns dict: Coding position model (c./r.). """ noncoding_pos_m = self._noncoding.to_position(coordinate) - location = noncoding_pos_m["position"] - if noncoding_pos_m["region"] == "": - if location < self._coding[0]: - return { - "position": self._coding[0] - location, - "offset": noncoding_pos_m["offset"], - "region": "-" - } - elif location >= self._coding[1]: - return { - "position": location - self._coding[1] + 1, - "offset": noncoding_pos_m["offset"], - "region": "*" - } + if noncoding_pos_m["region"] in ["u", "d"]: + return noncoding_pos_m + + location = noncoding_pos_m["position"] + offset = noncoding_pos_m["offset"] + if location < self._coding[0]: + return { + "position": self._coding[0] - location, + "offset": offset, + "region": "-" + } + if location >= self._coding[1]: return { - "position": location - self._coding[0] + 1, - "offset": noncoding_pos_m["offset"], - "region": "" + "position": location - self._coding[1] + 1, + "offset": offset, + "region": "*" } - else: - return noncoding_pos_m + return { + "position": location - self._coding[0] + 1, + "offset": offset, + "region": "" + } def coordinate_to_coding(self, coordinate, degenerate=False): """Convert a coordinate to a coding position (c./r.). @@ -121,71 +162,45 @@ def coordinate_to_coding(self, coordinate, degenerate=False): :returns dict: Coding position model (c./r.). """ pos_m = self._coordinate_to_coding(coordinate) - region = pos_m["region"] - position = pos_m["position"] - if degenerate: - if region == "u": - if self._inverted: - pos_m["position"] = position + self._exons[1] - self._coding[1] + 1 - else: - pos_m["position"] = position + self._coding[0] - pos_m["region"] = "-" - if region == "d": - if self._inverted: - pos_m["position"] = position + self._coding[0] - else: - pos_m["position"] = position + self._exons[1]- self._coding[1] + 1 - pos_m["region"] = "*" + if degenerate and pos_m["region"] in ("u", "d"): + pos_m = self._degenerate_position(pos_m) + return pos_m - def coding_to_coordinate(self, pos_m): + def _coding_to_coordinate(self, pos_m): """Convert a coding position (c./r.) to a coordinate. :arg dict pos_m: Coding position model (c./r.). :returns int: Coordinate. """ - region = pos_m["region"] position = pos_m["position"] - offset = pos_m["offset"] + region = pos_m["region"] if region in ["u", "d"]: return self._noncoding.to_coordinate(pos_m) - elif region == "": - noncoding_pos_m = { - "position": position + self._coding[0] -1, - "offset": offset, - "region": "" - } + + noncoding_pos_m = {"offset": pos_m["offset"], "region": ""} + if region == "": + noncoding_pos_m["position"] = position + self._coding[0] - 1 elif region == "-": - if position > self._coding[0]: # correct it to 'u' - noncoding_pos_m = { - "position": position - self._coding[0] - offset, - "offset": 0, - "region": "u" - } - else: - noncoding_pos_m = { - "position": self._coding[0] - position, - "offset": offset, - "region": "" - } - else: # * - if position > self._coding[0]: # correct it to 'd' - noncoding_pos_m = { - "position": position - self._coding[0] + offset, - "offset": 0, - "region": "d" - } - else: - noncoding_pos_m = { - "position": self._coding[1] + position - 1, - "offset": offset, - "region": "" - } + noncoding_pos_m["position"] = self._coding[0] - position + else: + noncoding_pos_m["position"] = self._coding[1] + position - 1 + return self._noncoding.to_coordinate(noncoding_pos_m) + def coding_to_coordinate(self, pos_m): + """Convert a coding position (c./r.) to a coordinate. + + :arg dict pos_m: Coding position model (c./r.). + + :returns int: Coordinate. + """ + normalized_pos_m = self._normalize_position(pos_m) + + return self._coding_to_coordinate(normalized_pos_m) def coordinate_to_protein(self, coordinate): """Convert a coordinate to a protein position (p.). @@ -198,35 +213,18 @@ def coordinate_to_protein(self, coordinate): if pos["region"] == "u": pos = self.coordinate_to_coding(coordinate + pos["position"]) - return { - "position": pos["position"] // 3 + 1, - "position_in_codon": pos["position"] % 3, - "region": "u", - **{k: v for k, v in pos.items() if k not in ["position", "region"]}} - - if pos["region"] == "d": + elif pos["region"] == "d": pos = self.coordinate_to_coding(coordinate - pos["position"]) - return { - "position": pos["position"] // 3 + 1, - "position_in_codon": pos["position"] % 3, - "region": "d", - **{k: v for k, v in pos.items() if k not in ["position", "region"]}} + position = pos["position"] if pos["region"] == "-": return { - "position": (pos["position"]+2) // 3, - "position_in_codon": -pos["position"] % 3 + 1, + "position": abs(-position // 3), + "position_in_codon": -position % 3 + 1, **{k: v for k, v in pos.items() if k != "position"}} - - if pos["region"] == "*": - return { - "position": (pos["position"]+2)// 3, - "position_in_codon": (pos["position"] + 2) % 3 + 1, - **{k: v for k, v in pos.items() if k != "position"}} - return { - "position": (pos["position"]+2) // 3, - "position_in_codon": (pos["position"]+2) % 3 + 1, + "position": (position + 2) // 3, + "position_in_codon": (position + 2) % 3 + 1, **{k: v for k, v in pos.items() if k != "position"}} def protein_to_coordinate(self, pos_m): @@ -241,6 +239,7 @@ def protein_to_coordinate(self, pos_m): {"position": 3 * pos_m["position"] - pos_m["position_in_codon"] + 1, "offset": pos_m["offset"], "region": pos_m["region"]}) + return self.coding_to_coordinate( {"position": 3 * pos_m["position"] + pos_m["position_in_codon"] - 3, "offset": pos_m["offset"], diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 55a9a92..a3bc5f7 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -105,8 +105,8 @@ def test_NonCoding_degenerate(): crossmap.noncoding_to_coordinate, 4, [ - {"position": 1, "offset": 0, "region": "u"}, {"position": 1, "offset": -1, "region": ""}, + {"position": 1, "offset": 0, "region": "u"}, ], ) @@ -116,6 +116,7 @@ def test_NonCoding_degenerate(): 72, [ {"position": 1, "offset": 0, "region": "d"}, + {"position": 22, "offset": 1, "region": ""}, {"position": 23, "offset": 0, "region": ""}, ], ) @@ -131,7 +132,8 @@ def test_NonCoding_inverted_degenerate(): 72, [ {"position": 1, "offset": -1, "region": ""}, - {"position": 1, "offset": 0, "region": "u"},], + {"position": 1, "offset": 0, "region": "u"}, + ], ) # Boundary between downstream and transcript. @@ -140,7 +142,9 @@ def test_NonCoding_inverted_degenerate(): 4, [ {"position": 1, "offset": 0, "region": "d"}, - {"position": 22, "offset": 1, "region": ""},], + {"position": 23, "offset": 0, "region": ""}, + {"position": 22, "offset": 1, "region": ""}, + ], ) @@ -464,16 +468,21 @@ def test_Coding_degenerate(): {"position": 2, "offset": 0, "region": "-"}, {"position": 1, "offset": -2, "region": ""}, {"position": 1, "offset": -10, "region": "*"}, + {"position": 2, "offset": -11, "region": "*"}, + {"position": 3, "offset": 1, "region": "-"}, + {"position": 4, "offset": 2, "region": "-"}, ], ) degenerate_equal( crossmap.coding_to_coordinate, 20, [ - {"position": 1, "offset": 1, "region": "*"}, + {"position": 1, "offset": 0, "region": "d"}, {"position": 2, "offset": 0, "region": "*"}, {"position": 8, "offset": 2, "region": ""}, {"position": 1, "offset": 10, "region": "-"}, + {"position": 2, "offset": 11, "region": "-"}, + {"position": 7, "offset": 3, "region": ""}, ], ) @@ -490,6 +499,8 @@ def test_Coding_inverted_degenerate(): {"position": 2, "offset": 0, "region": "-"}, {"position": 1, "offset": -2, "region": ""}, {"position": 1, "offset": -10, "region": "*"}, + {"position": 1, "offset": -11, "region": "d"}, + {"position": 2, "offset": -3, "region": ""}, ], ) degenerate_equal( @@ -500,6 +511,8 @@ def test_Coding_inverted_degenerate(): {"position": 2, "offset": 0, "region": "*"}, {"position": 8, "offset": 2, "region": ""}, {"position": 1, "offset": 10, "region": "-"}, + {"position": 1, "offset": 11, "region": "u"}, + {"position": 2, "offset": 12, "region": "u"}, ], ) @@ -563,6 +576,8 @@ def test_Coding_no_utr_degenerate(): {"position": 1, "offset": 0, "region": "u"}, {"position": 1, "offset": 0, "region": "-"}, {"position": 1, "offset": -2, "region": "*"}, + {"position": 1, "offset": -1, "region": ""}, + {"position": 1, "offset": -2, "region": "d"}, ], ) degenerate_equal( @@ -572,6 +587,8 @@ def test_Coding_no_utr_degenerate(): {"position": 1, "offset": 0, "region": "d"}, {"position": 1, "offset": 0, "region": "*"}, {"position": 1, "offset": 2, "region": "-"}, + {"position": 1, "offset": 1, "region": ""}, + {"position": 1, "offset": 2, "region": "u"}, ], ) @@ -587,6 +604,8 @@ def test_Coding_inverted_no_utr_degenerate(): {"position": 1, "offset": 0, "region": "u"}, {"position": 1, "offset": 0, "region": "-"}, {"position": 1, "offset": -2, "region": "*"}, + {"position": 1, "offset": -1, "region": ""}, + {"position": 1, "offset": -2, "region": "d"}, ], ) degenerate_equal( @@ -596,6 +615,8 @@ def test_Coding_inverted_no_utr_degenerate(): {"position": 1, "offset": 0, "region": "d"}, {"position": 1, "offset": 0, "region": "*"}, {"position": 1, "offset": 2, "region": "-"}, + {"position": 1, "offset": 1, "region": ""}, + {"position": 1, "offset": 2, "region": "u"}, ], ) From 186e2a69c0029e73b0e5a0b5f171cfa09dd05686 Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:52:55 +0100 Subject: [PATCH 074/236] Update tables in documentation --- README.rst | 70 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/README.rst b/README.rst index 29f15d1..cc007b2 100644 --- a/README.rst +++ b/README.rst @@ -129,13 +129,14 @@ Here is the mapping of coordinates to noncoding positions: :class: table-scroll :header: "Coordinate", "Position", "Offset", "Region", "HGVS" - "4", "1","0", "'u'", "n.u1" - "5", "1","0", "''", "n.1" - "24", "9","5", "''", "n.9+5" - "25", "10","-5", "''", "n.10-5" - "71", "22","0", "''", "n.22" - "72", "1","0", "'d'", "n.d1" - "79", "8","0", "'d'", "n.d8" + "0", "5", "0", "u", "c.u5" + "4", "1", "0", "u", "n.u1" + "5", "1", "0", "", "n.1" + "24", "9", "5", "", "n.9+5" + "25", "10", "-5", "", "n.10-5" + "71", "22", "0", "", "n.22" + "72", "1", "0", "d", "n.d1" + "79", "8", "0", "d", "n.d8" @@ -154,7 +155,7 @@ Coding positions follow the HGVS ``c`` coordinate system. They are represented a { "position": 1, "offset": 3, - "region": '*' + "region": "*" } Where: @@ -186,19 +187,20 @@ Here is the mapping of coordinates to coding positions: :class: table-scroll :header: "Coordinate", "Position", "Offset", "Region", "HGVS" - "4", "1","0", "'u'", "c.u1" - "5", "11","0", '-', "c.-11" - "24", "3","5", '-', "c.-3+5" - "25", "2","-5", '-', "c.-2-5" - "31", "1","0", '-', "c.-1" - "32", "1","0", "''", "c.1" - "37", "3","3", "''", "c.3+3" - "38", "4","-2", "''", "c.4-2" - "43", "1","0", '*', "c.*1" - "60", "3","9", '*', "c.*3+9" - "61", "4","-9", '*', "c.*4+9" - "71", "5","0", '*', "c.*5" - "79", "8","0", "'d'", "c.d8" + "0", "5", "0", "u", "c.u5" + "4", "1", "0", "u", "c.u1" + "5", "11", "0", "\-", "c.-11" + "24", "3", "5", "\-", "c.-3+5" + "25", "2", "-5", "\-", "c.-2-5" + "31", "1", "0", "\-", "c.-1" + "32", "1", "0", "", "c.1" + "37", "3", "3", "", "c.3+3" + "38", "4", "-2", "", "c.4-2" + "43", "1", "0", "\*", "c.*1" + "60", "3", "9", "\*", "c.*3+9" + "61", "4", "-9", "\*", "c.*4+9" + "71", "5", "0", "\*", "c.*5" + "79", "8", "0", "d", "c.d8" @@ -244,19 +246,19 @@ Here is the mapping of coordinates to protein positions: .. csv-table:: :class: table-scroll - :header: "Coordinate", "Position", "position_in_codon", "Offset", "Region", "HGVS" - - "0", "4", "2", "0", "'u'", - "4", "4", "2", "0", "'u'", - "5", "4", "2", "0", '-', - "6", "4", "3", "0", '-', - "7", "3", "1", "0", '-', - "31", "1", "3", "0", '-', - "32", "1", "1", "0", "''", "p.1" - "42", "2", "3", "0", "''", "p.2" - "43", "1", "1", "0", '*', - "44", "1", "1", "1", '*', - "79", "2", "2", "0", "'d'", + :header: "Coordinate", "Position", "position_in_codon", "Offset", "Region", "HGVS" + + "0", "4", "2", "0", "u", + "4", "4", "2", "0", "u", + "5", "4", "2", "0", "\-", + "6", "4", "3", "0", "\-", + "7", "3", "1", "0", "\-", + "31", "1", "3", "0", "\-", + "32", "1", "1", "0", "", "p.1" + "42", "2", "3", "0", "", "p.2" + "43", "1", "1", "0", "\*", + "44", "1", "1", "1", "\*", + "79", "2", "2", "0", "d", From 617b126d548c540d369d36ceea919474ec1ed2dc Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:54:28 +0100 Subject: [PATCH 075/236] Update README.rst --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index cc007b2..79e9c48 100644 --- a/README.rst +++ b/README.rst @@ -115,7 +115,7 @@ NonCoding Position Conversion >>> crossmap = NonCoding(_exons) >>> crossmap.coordinate_to_noncoding(25) {"position": 10, "offset": -5, "region": ''} - >>> crossmap.noncoding_to_coordinate({"position": 10, "offset": -5, "region": ''}) + >>> crossmap.noncoding_to_coordinate({"position": 10, "offset": -5, "region": ""}) 25 Notes @@ -173,7 +173,7 @@ Coding Position Conversion >>> crossmap = Coding(_exons, _cds) >>> crossmap.coordinate_to_coding(46) {"position": 1, "offset": 3, "region": '*'} - >>> crossmap.coding_to_coordinate({"position": 1, "offset": 3, "region": '*'}) + >>> crossmap.coding_to_coordinate({"position": 1, "offset": 3, "region": "*"}) 46 Notes From 4fddbc316100af52b75e570b6de9aa15f89569db Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 20 Mar 2026 10:47:40 +0100 Subject: [PATCH 076/236] Use single quate for dictionary --- mutalyzer_crossmapper/crossmapper.py | 130 +++++++++++++-------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 0fd0c09..3994271 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -10,7 +10,7 @@ def coordinate_to_genomic(self, coordinate): :returns dict: Genomic position model. """ - return {"position": coordinate + 1} + return {'position': coordinate + 1} def genomic_to_coordinate(self, pos_m): """Convert a genomic position (g./m./o.) to a coordinate. @@ -19,7 +19,7 @@ def genomic_to_coordinate(self, pos_m): :returns int: Coordinate. """ - return pos_m["position"] - 1 + return pos_m['position'] - 1 class NonCoding(Genomic): @@ -41,8 +41,8 @@ def coordinate_to_noncoding(self, coordinate): :returns dict: Noncoding position model. """ pos_m = self._noncoding.to_position(coordinate) - if pos_m["region"] == "": - pos_m["position"] = pos_m["position"] + 1 + if pos_m['region'] == '': + pos_m['position'] = pos_m['position'] + 1 return pos_m def noncoding_to_coordinate(self, pos_m): @@ -53,8 +53,8 @@ def noncoding_to_coordinate(self, pos_m): :returns int: Coordinate. """ multilocus_pos_m = {**pos_m} - if pos_m["region"] == "": - multilocus_pos_m["position"] = pos_m["position"] - 1 + if pos_m['region'] == '': + multilocus_pos_m['position'] = pos_m['position'] - 1 return self._noncoding.to_coordinate(multilocus_pos_m) @@ -74,11 +74,11 @@ def __init__(self, locations, cds, inverted=False): e1 = self._noncoding.to_position(locations[-1][1]-1) if self._inverted: - self._coding = (b1["position"] + b1["offset"], b0["position"] + b0["offset"] + 1) - self._exons = (e1["position"], e0["position"]) + self._coding = (b1['position'] + b1['offset'], b0['position'] + b0['offset'] + 1) + self._exons = (e1['position'], e0['position']) else: - self._coding = (b0["position"] + b0["offset"], b1["position"] + b1["offset"] +1) - self._exons = (e0["position"], e1["position"]) + self._coding = (b0['position'] + b0['offset'], b1['position'] + b1['offset'] +1) + self._exons = (e0['position'], e1['position']) def _degenerate_position(self, pos_m): """Degenerate a coding position model (c./r.). @@ -87,23 +87,23 @@ def _degenerate_position(self, pos_m): :returns dict: a generate coding position model. """ - region = pos_m["region"] - position = pos_m["position"] + region = pos_m['region'] + position = pos_m['position'] - degenerated_pos_m = {"offset": pos_m["offset"]} + degenerated_pos_m = {'offset': pos_m['offset']} - if region == "u": + if region == 'u': if self._inverted: - degenerated_pos_m["position"] = position + self._exons[1] - self._coding[1] + 1 + degenerated_pos_m['position'] = position + self._exons[1] - self._coding[1] + 1 else: - degenerated_pos_m["position"] = position + self._coding[0] - degenerated_pos_m["region"] = "-" - if region == "d": + degenerated_pos_m['position'] = position + self._coding[0] + degenerated_pos_m['region'] = '-' + if region == 'd': if self._inverted: - degenerated_pos_m["position"] = position + self._coding[0] + degenerated_pos_m['position'] = position + self._coding[0] else: - degenerated_pos_m["position"] = position + self._exons[1]- self._coding[1] + 1 - degenerated_pos_m["region"] = "*" + degenerated_pos_m['position'] = position + self._exons[1]- self._coding[1] + 1 + degenerated_pos_m['region'] = '*' return degenerated_pos_m def _normalize_position(self, pos_m): @@ -113,12 +113,12 @@ def _normalize_position(self, pos_m): :returns dict: a normalized coding postion model. """ - initial_pos = {**pos_m, "offset": 0} + initial_pos = {**pos_m, 'offset': 0} coordinate = self._coding_to_coordinate(initial_pos) if self._inverted: - coordinate = coordinate - pos_m["offset"] + coordinate = coordinate - pos_m['offset'] else: - coordinate = coordinate + pos_m["offset"] + coordinate = coordinate + pos_m['offset'] return self.coordinate_to_coding(coordinate) def _coordinate_to_coding(self, coordinate): @@ -130,27 +130,27 @@ def _coordinate_to_coding(self, coordinate): """ noncoding_pos_m = self._noncoding.to_position(coordinate) - if noncoding_pos_m["region"] in ["u", "d"]: + if noncoding_pos_m['region'] in ['u', 'd']: return noncoding_pos_m - location = noncoding_pos_m["position"] - offset = noncoding_pos_m["offset"] + location = noncoding_pos_m['position'] + offset = noncoding_pos_m['offset'] if location < self._coding[0]: return { - "position": self._coding[0] - location, - "offset": offset, - "region": "-" + 'position': self._coding[0] - location, + 'offset': offset, + 'region': '-' } if location >= self._coding[1]: return { - "position": location - self._coding[1] + 1, - "offset": offset, - "region": "*" + 'position': location - self._coding[1] + 1, + 'offset': offset, + 'region': '*' } return { - "position": location - self._coding[0] + 1, - "offset": offset, - "region": "" + 'position': location - self._coding[0] + 1, + 'offset': offset, + 'region': '' } def coordinate_to_coding(self, coordinate, degenerate=False): @@ -163,7 +163,7 @@ def coordinate_to_coding(self, coordinate, degenerate=False): """ pos_m = self._coordinate_to_coding(coordinate) - if degenerate and pos_m["region"] in ("u", "d"): + if degenerate and pos_m['region'] in ('u', 'd'): pos_m = self._degenerate_position(pos_m) return pos_m @@ -175,19 +175,19 @@ def _coding_to_coordinate(self, pos_m): :returns int: Coordinate. """ - position = pos_m["position"] - region = pos_m["region"] + position = pos_m['position'] + region = pos_m['region'] - if region in ["u", "d"]: + if region in ['u', 'd']: return self._noncoding.to_coordinate(pos_m) - noncoding_pos_m = {"offset": pos_m["offset"], "region": ""} - if region == "": - noncoding_pos_m["position"] = position + self._coding[0] - 1 - elif region == "-": - noncoding_pos_m["position"] = self._coding[0] - position + noncoding_pos_m = {'offset': pos_m['offset'], 'region': ''} + if region == '': + noncoding_pos_m['position'] = position + self._coding[0] - 1 + elif region == '-': + noncoding_pos_m['position'] = self._coding[0] - position else: - noncoding_pos_m["position"] = self._coding[1] + position - 1 + noncoding_pos_m['position'] = self._coding[1] + position - 1 return self._noncoding.to_coordinate(noncoding_pos_m) @@ -211,21 +211,21 @@ def coordinate_to_protein(self, coordinate): """ pos = self.coordinate_to_coding(coordinate) - if pos["region"] == "u": - pos = self.coordinate_to_coding(coordinate + pos["position"]) - elif pos["region"] == "d": - pos = self.coordinate_to_coding(coordinate - pos["position"]) + if pos['region'] == 'u': + pos = self.coordinate_to_coding(coordinate + pos['position']) + elif pos['region'] == 'd': + pos = self.coordinate_to_coding(coordinate - pos['position']) - position = pos["position"] - if pos["region"] == "-": + position = pos['position'] + if pos['region'] == '-': return { - "position": abs(-position // 3), - "position_in_codon": -position % 3 + 1, - **{k: v for k, v in pos.items() if k != "position"}} + 'position': abs(-position // 3), + 'position_in_codon': -position % 3 + 1, + **{k: v for k, v in pos.items() if k != 'position'}} return { - "position": (position + 2) // 3, - "position_in_codon": (position + 2) % 3 + 1, - **{k: v for k, v in pos.items() if k != "position"}} + 'position': (position + 2) // 3, + 'position_in_codon': (position + 2) % 3 + 1, + **{k: v for k, v in pos.items() if k != 'position'}} def protein_to_coordinate(self, pos_m): """Convert a protein position (p.) to a coordinate. @@ -234,13 +234,13 @@ def protein_to_coordinate(self, pos_m): :returns int: Coordinate. """ - if pos_m["region"] == "-": + if pos_m['region'] == '-': return self.coding_to_coordinate( - {"position": 3 * pos_m["position"] - pos_m["position_in_codon"] + 1, - "offset": pos_m["offset"], - "region": pos_m["region"]}) + {'position': 3 * pos_m['position'] - pos_m['position_in_codon'] + 1, + 'offset': pos_m['offset'], + 'region': pos_m['region']}) return self.coding_to_coordinate( - {"position": 3 * pos_m["position"] + pos_m["position_in_codon"] - 3, - "offset": pos_m["offset"], - "region": pos_m["region"]}) + {'position': 3 * pos_m['position'] + pos_m['position_in_codon'] - 3, + 'offset': pos_m['offset'], + 'region': pos_m['region']}) From f85b25f5a73e588fd07d887ac8e2082b6161c206 Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:59:20 +0100 Subject: [PATCH 077/236] Delete table --- README.rst | 251 ++++++++++------------------------------------------- 1 file changed, 45 insertions(+), 206 deletions(-) diff --git a/README.rst b/README.rst index 79e9c48..d398402 100644 --- a/README.rst +++ b/README.rst @@ -27,239 +27,78 @@ HGVS position crossmapper This library provides an interface to convert (cross map) between different HGVS numbering_ systems. -Converting between the transcript oriented c. or n. and the genomic oriented g. +Converting between the transcript oriented ``c.`` or ``n.`` and the genomic oriented ``g.`` numbering systems can be difficult, especially when the transcript in question -resides on the complement strand. +resides on the complement strand. This library provides functions to convert between any HGVS +numbering system to standard (0-based) coordinates and vice versa. **Features:** -- Support for genomic positions to standard coordinates and vice versa. -- Support for noncoding positions to standard coordinates and vice versa. -- Support for coding positions to standard coordinates and vice versa. -- Support for protein positions to standard coordinates and vice versa. -- Basic classes for loci that can be used for genomic loci other than genes. +- Support for genomic (``g.``, ``m.``, ``o.``) positions to standard coordinates and vice versa. +- Support for noncoding (``n.``, ``r.``) positions to standard coordinates and vice versa. +- Support for coding (``c.``, ``r.``) positions to standard coordinates and vice versa. +- Support for protein (``p.``) positions to standard coordinates and vice versa. +- Basic classes that can be used for loci other than genes or transcripts. Please see ReadTheDocs_ for the latest documentation. -Quick Start -=========== +Quick start +----------- -An example below uses the following transcript data: +The ``Genomic`` class provides an interface to conversions between genomic +positions and coordinates. -.. code-block:: python - - >>>_exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] - >>>_cds = (32, 43) - - -Genomic Class -------------- - -The ``Genomic`` class provides an interface for conversions between genomic positions and coordinates. - -Genomic Position Model -~~~~~~~~~~~~~~~~~~~~~~~ - -Genomic positions follow the HGVS ``g`` coordinate system. They are represented as dictionaries. Below is an example of `g.1` in HGVS. - -.. code-block:: json - - {"position": 1} - -Where: - -- **position**: a positive integer(>0) - -Genomic Position Conversion -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: python +.. code:: python >>> from mutalyzer_crossmapper import Genomic >>> crossmap = Genomic() >>> crossmap.coordinate_to_genomic(0) - {"position": 1} - >>> crossmap.genomic_to_coordinate({"position": 1}) + 1 + >>> crossmap.genomic_to_coordinate({'position': 1}) 0 -NonCoding Class ---------------- - -The ``NonCoding`` class provides conversions between noncoding positions and coordinates. - -NonCoding Position Model -~~~~~~~~~~~~~~~~~~~~~~~ +On top of the functionality provided by the ``Genomic`` class, the +``NonCoding`` class provides an interface to conversions between noncoding +positions and coordinates. -Noncoding positions follow the HGVS ``n`` coordinate system. They are represented as dictionaries. Below is an example of ``n.14+1`` in HGVS. - -.. code-block:: json - - { - "position": 10, - "offset": -5, - "region": "" - } - -Where: - -- **position**: a positive integer (>0) -- **offset**: an integer indicating the offset relative to the position (negative for upstream, positive for downstream) -- **region**: a string describing the region type (``''`` for standard, ``'u'`` for upstream, ``'d'`` for downstream) - -NonCoding Position Conversion -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: python +.. code:: python >>> from mutalyzer_crossmapper import NonCoding - >>> crossmap = NonCoding(_exons) - >>> crossmap.coordinate_to_noncoding(25) - {"position": 10, "offset": -5, "region": ''} - >>> crossmap.noncoding_to_coordinate({"position": 10, "offset": -5, "region": ""}) - 25 - -Notes -~~~~~ - -- Add the flag ``inverted=True`` to the constructor when the transcript resides on the reverse complement strand. - -Here is the mapping of coordinates to noncoding positions: - -.. csv-table:: - :class: table-scroll - :header: "Coordinate", "Position", "Offset", "Region", "HGVS" - - "0", "5", "0", "u", "c.u5" - "4", "1", "0", "u", "n.u1" - "5", "1", "0", "", "n.1" - "24", "9", "5", "", "n.9+5" - "25", "10", "-5", "", "n.10-5" - "71", "22", "0", "", "n.22" - "72", "1", "0", "d", "n.d1" - "79", "8", "0", "d", "n.d8" - - + >>> exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] + >>> crossmap = NonCoding(exons) + >>> crossmap.coordinate_to_noncoding(35) + {'position':14, 'offset':1, 'region':''} + >>> crossmap.noncoding_to_coordinate({'position':14, 'offset':1, 'region':''}) + 35 -Coding Class ------------- +Add the flag ``inverted=True`` to the constructor when the transcript resides +on the reverse complement strand. -The ``Coding`` class provides conversions between coding positions and coordinates, as well as protein positions. +On top of the functionality provided by the ``NonCoding`` class, the ``Coding`` +class provides an interface to conversions between coding positions and +coordinates as well as conversions between protein positions and coordinates. -Coding Position Model -~~~~~~~~~~~~~~~~~~~~ - -Coding positions follow the HGVS ``c`` coordinate system. They are represented as dictionaries. Here is an example of ``c.*1+3``. - -.. code-block:: json - - { - "position": 1, - "offset": 3, - "region": "*" - } - -Where: - -- **position**: a positive integer -- **offset**: an integer indicating the offset relative to the position -- **region**: a string describing the region type (`""` for standard coding positions, `'-'` for 5' UTR, `'*'` for 3' UTR, `'u'` for upstream and ``"d"`` for downstream) - -Coding Position Conversion -~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: python +.. code:: python >>> from mutalyzer_crossmapper import Coding - >>> crossmap = Coding(_exons, _cds) - >>> crossmap.coordinate_to_coding(46) - {"position": 1, "offset": 3, "region": '*'} - >>> crossmap.coding_to_coordinate({"position": 1, "offset": 3, "region": "*"}) - 46 - -Notes -~~~~~ - -- The flag ``inverted=True`` can be used for transcripts on the reverse complement strand. - -Here is the mapping of coordinates to coding positions: - -.. csv-table:: - :class: table-scroll - :header: "Coordinate", "Position", "Offset", "Region", "HGVS" - - "0", "5", "0", "u", "c.u5" - "4", "1", "0", "u", "c.u1" - "5", "11", "0", "\-", "c.-11" - "24", "3", "5", "\-", "c.-3+5" - "25", "2", "-5", "\-", "c.-2-5" - "31", "1", "0", "\-", "c.-1" - "32", "1", "0", "", "c.1" - "37", "3", "3", "", "c.3+3" - "38", "4", "-2", "", "c.4-2" - "43", "1", "0", "\*", "c.*1" - "60", "3", "9", "\*", "c.*3+9" - "61", "4", "-9", "\*", "c.*4+9" - "71", "5", "0", "\*", "c.*5" - "79", "8", "0", "d", "c.d8" - - - - -Protein -------- - -Protein Position Model -~~~~~~~~~~~~~~~~~~~~~~ - -Protein positions follow the HGVS ``p`` coordinate system. They are represented as dictionaries. Here is an example of ``p.1`` in HGVS. - -.. code-block:: json - - { - "position": 1, - "position_in_codon": 3, - "offset": 3, - "region": "" - } - -Where: - -- **position**: the amino acid position (1-based) -- **position_in_codon**: the codon nucleotide index (1, 2, or 3) -- **offset**: an integer indicating offset relative to the codon -- **region**: a string describing the region type (``''`` for standard positions) - -Protein Position Conversion -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Conversions between protein positions and coordinates: - -.. code-block:: python - - >>> crossmap.coordinate_to_protein(37) - {"position": 1, "position_in_codon": 3, "offset": 3, "region": ""} - >>> crossmap.protein_to_coordinate({"position": 1, "position_in_codon": 3, "offset": 3, "region": ""}) - 37 - + >>> cds = (32, 43) + >>> crossmap = Coding(exons, cds) + >>> crossmap.coordinate_to_coding(31) + {'position':1, 'offset':0, 'region':'-'} + >>> crossmap.coding_to_coordinate({'position':1, 'offset':0, 'region':'-'}) + 31 -Here is the mapping of coordinates to protein positions: +Again, the flag ``inverted=True`` can be used for transcripts that reside on +the reverse complement strand. -.. csv-table:: - :class: table-scroll - :header: "Coordinate", "Position", "position_in_codon", "Offset", "Region", "HGVS" +Conversions between protein positions and coordinates are done as follows. - "0", "4", "2", "0", "u", - "4", "4", "2", "0", "u", - "5", "4", "2", "0", "\-", - "6", "4", "3", "0", "\-", - "7", "3", "1", "0", "\-", - "31", "1", "3", "0", "\-", - "32", "1", "1", "0", "", "p.1" - "42", "2", "3", "0", "", "p.2" - "43", "1", "1", "0", "\*", - "44", "1", "1", "1", "\*", - "79", "2", "2", "0", "d", +.. code:: python + >>> crossmap.coordinate_to_protein(41) + {'position':2, 'position_in_codon': 2, 'offset':0, 'region':''} + >>> crossmap.protein_to_coordinate({'position':2, 'position_in_codon': 2, 'offset':0, 'region':''}) + 41 From ca4379ef865138c0d66a9436f102497b1f91dbf5 Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:32:56 +0100 Subject: [PATCH 078/236] Update README.rst --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index d398402..fd0308c 100644 --- a/README.rst +++ b/README.rst @@ -54,7 +54,7 @@ positions and coordinates. >>> crossmap = Genomic() >>> crossmap.coordinate_to_genomic(0) 1 - >>> crossmap.genomic_to_coordinate({'position': 1}) + >>> crossmap.genomic_to_coordinate({'position':1}) 0 On top of the functionality provided by the ``Genomic`` class, the @@ -97,7 +97,7 @@ Conversions between protein positions and coordinates are done as follows. >>> crossmap.coordinate_to_protein(41) {'position':2, 'position_in_codon': 2, 'offset':0, 'region':''} - >>> crossmap.protein_to_coordinate({'position':2, 'position_in_codon': 2, 'offset':0, 'region':''}) + >>> crossmap.protein_to_coordinate({'position':2, 'position_in_codon':2, 'offset':0, 'region':''}) 41 From 072719d7dd1a1c27b7be8adb1e234dca56d7cc78 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 20 Mar 2026 13:32:07 +0100 Subject: [PATCH 079/236] Update library, replace tuple with dictionary --- docs/library.rst | 308 ++++++++++++++++++++++++++--------------------- 1 file changed, 173 insertions(+), 135 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 6ef095a..032b5c8 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -8,7 +8,24 @@ The ``Genomic`` class --------------------- The ``Genomic`` class provides an interface to conversions between genomic -positions and coordinates. +(``g.``, ``m``, ``n``) positions and coordinates. + +Genomic Position Model +~~~~~~~~~~~~~~~~~~~~~~~ + +Genomic positions follow the HGVS genomic coordinate system. +They are represented as 1-key dictionaries. Below is an example of `g.1` in HGVS. + +.. code-block:: json + + {'position':1} + +Where: + +- **position**: a positive integer(>0) + +Genomic Position Conversion +~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: python @@ -22,7 +39,7 @@ used to convert to and from genomic positions. >>> crossmap.coordinate_to_genomic(0) 1 - >>> crossmap.genomic_to_coordinate(1) + >>> crossmap.genomic_to_coordinate({'position':1}) 0 See section :doc:`api/crossmap` for a detailed description. @@ -32,8 +49,33 @@ The ``NonCoding`` class On top of the functionality provided by the ``Genomic`` class, the ``NonCoding`` class provides an interface to conversions between noncoding -positions and coordinates. Conversions between positioning systems should be -done via a coordinate. +(``n.``, ``r.``) positions and coordinates. Conversions between positioning +systems should be done via a coordinate. + +NonCoding Position Model +~~~~~~~~~~~~~~~~~~~~~~~~ + +Noncoding positions follow the HGVS ``n`` coordinate system. They are represented +as 3-key dictionaries. Below is an example of ``n.14+1`` in HGVS. + +.. code-block:: json + + { + 'position': 14, + 'offset': 1, + 'region': '' + } + +Where: + +- **position**: an interger representing a transcript position (>0) +- **offset**: an integer indicating the offset relative to the position (negative for upstream, + positive for downstream) +- **region**: a string describing the region type (``''`` for standard, ``'u'`` for upstream, + ``'d'`` for downstream) + +NonCoding Position Conversion +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: python @@ -41,22 +83,8 @@ done via a coordinate. >>> exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] >>> crossmap = NonCoding(exons) -Now the functions ``coordinate_to_noncoding()`` and -``noncoding_to_coordinate()`` can be used. These functions use a 3-tuple to -represent a noncoding position. - -.. _table_noncoding: -.. list-table:: Noncoding positions. - :header-rows: 1 - - * - index - - description - * - 0 - - Transcript position. - * - 1 - - Offset. - * - 2 - - Upstream or downstream offset. +Now the functions ``coordinate_to_noncoding()`` and ``noncoding_to_coordinate()`` +can be used. In our example, the HGVS position "g.36" (coordinate ``35``) is equivalent to position "n.14+1". We can convert between these two as follows. @@ -64,25 +92,24 @@ position "n.14+1". We can convert between these two as follows. .. code:: python >>> crossmap.coordinate_to_noncoding(35) - (14, 1, 0) + {'position':14, 'offset':1, 'region':''} + >>> crossmap.noncoding_to_coordinate({'position':14, 'offset':1, 'region':''}) + {'position':14, 'offset':1, 'region':''} -When the coordinate is upstream or downstream of the transcript, the last -element of the tuple denotes the offset with respect to the transcript. This -makes it possible to distinguish between intronic positions and those outside -of the transcript. +When the coordinate is upstream or downstream of the transcript, we use ``'u`` to +present upstream and ``'d'`` to present downstream. .. code:: python >>> crossmap.coordinate_to_noncoding(2) - (1, -3, -3) + {'position':3, 'offset':0, 'region':'u'} + >>> crossmap.noncoding_to_coordinate({'position':3, 'offset':0, 'region':'u'}) + 2 >>> crossmap.coordinate_to_noncoding(73) - (22, 2, 2) + {'position':2, 'offset':0, 'region':'d'} + >>> crossmap.noncoding_to_coordinate({'position':2, 'offset':0, 'region':'d'}) + 73 -Note that this last element is optional (and ignored) when a conversion to a -coordinate is requested. - - >>> crossmap.noncoding_to_coordinate((14, 1)) - 35 For transcripts that reside on the reverse complement strand, the ``inverted`` parameter should be set to ``True``. In our example, HGVS position "g.36" @@ -92,18 +119,55 @@ parameter should be set to ``True``. In our example, HGVS position "g.36" >>> crossmap = NonCoding(exons, inverted=True) >>> crossmap.coordinate_to_noncoding(35) - (9, -1, 0) - >>> crossmap.noncoding_to_coordinate((9, -1)) + {'position':9, 'offset':-1, 'region':''} + >>> crossmap.noncoding_to_coordinate({'position':9, 'offset':-1, 'region':''}) 35 +In the following table, we show a number of annotated examples. +.. csv-table:: + :class: table-scroll + :header: "Coordinate", "Position", "Offset", "Region", "HGVS" + + "0", "5", "0", `u`, `n.u5` + "4", "1", "0", `u`, `n.u1` + "5", "1", "0", `""`, `n.1` + "24", "9", "5", `""`, `n.9+5` + "25", "10", "-5", `""`, `n.10-5` + "71", "22", "0", `""`, `n.22` + "72", "1", "0", `d`, `n.d1` + "79", "8", "0", `d`, `n.d8` + See section :doc:`api/crossmap` for a detailed description. The ``Coding`` class -------------------- The ``Coding`` class provides an interface to all conversions between -positioning systems and coordinates. Conversions between positioning systems -should be done via a coordinate. +coding (``c.``, ``r.``) rpositioning systems and coordinates. Conversions between +positioning systems should be done via a coordinate. + +Coding Position Model +~~~~~~~~~~~~~~~~~~~~~ +Coding positions follow the HGVS ``c`` coordinate system. They are +represented as 3-key dictionaries. Here is an example of ``c.*1+3``. + +.. code-block:: json + + { + 'position': 1, + 'offset': 3, + 'region': '*' + } + +Where: + +- **position**: an interger representing a transcript position (>0) +- **offset**: an integer indicating the offset relative to the position +- **region**: a string describing the region type (`''` for standard coding positions, + `'-'` for 5' UTR, `'*'` for 3' UTR, `'u'` for upstream and ``'d'`` for downstream) + +Coding Position Conversion +~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: python @@ -114,40 +178,7 @@ should be done via a coordinate. On top of the functionality provided by the ``NonCoding`` class, the functions ``coordinate_to_coding()`` and ``coding_to_coordinate()`` can be used. These -functions use a 4-tuple to represent a coding position. - -.. list-table:: Coding positions. - :header-rows: 1 - - * - index - - description - * - 0 - - Transcript position. - * - 1 - - Offset. - * - 2 - - Region. - * - 3 - - Upstream or downstream offset. - -The region denotes the location of the position with respect to the CDS. This -is needed in order to work with the HGVS "-" and "*" positions. - -.. list-table:: Coding position regions. - :header-rows: 1 - - * - value - - description - - HGVS example - * - ``-1`` - - Upstream of the CDS. - - "c.-10" - * - ``0`` - - In the CDS. - - "c.1" - * - ``1`` - - Downstream of the CDS. - - "c.*10" +functions use a 3-key dictionary to represent a coding position. In our example, the HGVS position "g.32" (coordinate ``31``) is equivalent to position "c.-1". We can convert between these two as follows. @@ -155,40 +186,64 @@ position "c.-1". We can convert between these two as follows. .. code:: python >>> crossmap.coordinate_to_coding(31) - (-1, 0, -1, 0) - >>> crossmap.coding_to_coordinate((-1, 0, -1)) + {'position':1, 'offset':0, 'region':'-'} + >>> crossmap.coding_to_coordinate({'position':1, 'offset':0, 'region':'-'}) 31 The ``coordinate_to_coding()`` function accepts an optional ``degenerate`` argument. When set to ``True``, positions outside of the transcript are no -longer described using the offset notation. +longer described using the ``'u'`` or ``'d'`` notation. .. code:: python >>> crossmap.coordinate_to_coding(4) - (-11, -1, -1, -1) + {'position':1, 'offset':0, 'region':'u'} >>> crossmap.coordinate_to_coding(4, True) - (-12, 0, -1, -1) + {'position':12, 'offset':0, 'region':'-'} + +In the following table, we show a number of annotated examples. + +.. csv-table:: + :class: + :header: "Coordinate", "Position", "Offset", "Region", "HGVS" + + "0", "5", "0", `u`, `c.u5` + "4", "1", "0", `u`, `c.u1` + "5", "11", "0", `-`, `c.-11` + "24", "3", "5", `-`, `c.-3+5` + "31", "1", "0", `-`, `c.-1` + "32", "1", "0", `""`, `c.1` + "37", "3", "3", `""`, `c.3+3` + "38", "4", "-2", `""`, `c.4-2` + "43", "1", "0", `*`, `c.*1` + "61", "4", "-9", `*`, `c.*4+9` + "71", "5", "0", `*`, `c.*5` + "79", "8", "0", `d`, `c.d8` + + +Protein +------- Additionally, the functions ``coordinate_to_protein()`` and -``protein_to_coordinate()`` can be used. These functions use a 5-tuple to -represent a protein position. - -.. list-table:: Protein positions. - :header-rows: 1 - - * - index - - description - * - 0 - - Protein position. - * - 1 - - Codon position. - * - 2 - - Offset. - * - 3 - - Region. - * - 4 - - Upstream or downstream offset. +``protein_to_coordinate()`` can be used. These functions use a 4-key dictionary +to represent a protein position. Here is an example of ``p.1`` in HGVS. + +.. code-block:: json + + { + 'position': 1, + 'position_in_codon': 3, + 'offset': 3, + 'region': '' + } + +Where: + +- **position**: an interger representing the protein position (>0) +- **position_in_codon**: an integer indicating the nucleotide index within the codon (1, 2, or 3) +- **offset**: an integer indicating offset relative to the codon +- **region**: a string describing the region type (``''`` for standard positions) + In our example the HGVS position "g.42" (coordinate ``41``) corresponds with position "p.2". We can convert between these to as follows. @@ -196,49 +251,32 @@ position "p.2". We can convert between these to as follows. .. code:: python >>> crossmap.coordinate_to_protein(41) - (2, 2, 0, 0, 0) - >>> crossmap.protein_to_coordinate((2, 2, 0, 0)) + {'position':2, 'position_in_codon':2, 'offset':0, 'region':''} + >>> crossmap.protein_to_coordinate({'position':2, 'position_in_codon':2, offset':0, 'region':''}) 41 Note that the protein position only corresponds with the HGVS "p." notation when the offset equals ``0`` and the region equals ``1``. In the following table, we show a number of annotated examples. -.. list-table:: Protein positions examples. - :header-rows: 1 - - * - coordinate - - protein position - - description - - HGVS position - * - ``4`` - - ``(-4, 2, -1, -1, -1)`` - - Upstream position. - - invalid - * - ``31`` - - ``(-1, 3, 0, -1, 0)`` - - 5' UTR position. - - invalid - * - ``36`` - - ``(1, 3, 2, 0, 0)`` - - Intronic position. - - invalid - * - ``40`` - - ``(2, 1, 0, 0, 0)`` - - Second amino acid, first nucleotide. - - "p.2" - * - ``41`` - - ``(2, 2, 0, 0, 0)`` - - Second amino acid, second nucleotide. - - "p.2" - * - ``43`` - - ``(1, 1, 0, 1, 0)`` - - 3' UTR position. - - invalid - * - ``43`` - - ``(2, 2, 2, 1, 2)`` - - Downstream position. - - invalid +.. csv-table:: + :class: table-scroll + :header: "Coordinate", "Position", "position_in_codon", "Offset", "Region", "HGVS" + + "0", "4", "2", "0", `u`, `` + "4", "4", "2", "0", `u`, `` + "5", "4", "2", "0", `-`, `` + "6", "4", "3", "0", `-`, `` + "7", "3", "1", "0", `-`, `` + "31", "1", "3", "0", `-`, `` + "32", "1", "1", "0", ``, `p.1` + "33", "1", "2", "0", ``, `p.1` + "42", "2", "3", "0", ``, `p.2` + "43", "1", "1", "0", `*`, `` + "44", "1", "1", "1", `*`, `` + "79", "2", "2", "0", `d`, `` + + See section :doc:`api/crossmap` for a detailed description. @@ -279,7 +317,7 @@ The ``Locus`` class ^^^^^^^^^^^^^^^^^^^ The ``Locus`` class is used to deal with offsets with respect to a single -locus. +locus. .. code:: python @@ -288,13 +326,13 @@ locus. This class provides the functions ``to_position()`` and ``to_coordinate()`` for converting from a locus position to a coordinate and vice versa. These -functions work with a 2-tuple, see the section about `The NonCoding class`_ +functions work with a 2-key dictionary, see the section about `The NonCoding class`_ for the semantics. .. code:: python >>> locus.to_position(9) - (1, -1) + {'position':1, 'offset':-1} For loci that reside on the reverse complement strand, the optional ``inverted`` constructor parameter should be set to ``True``. @@ -317,8 +355,8 @@ The interface to this class is similar to that of the ``Locus`` class. .. code:: python >>> multilocus.to_position(22) - (10, 3) + {'position':10, 'offset':3, 'region':''} >>> multilocus.to_position(38) - (11, -2) + {'position':11, 'offset':-2, 'region':''} See section :doc:`api/multi_locus` for a detailed description. From fba6b95312134da10a4b5b41c0e74c9829dbe2c8 Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:16:35 +0100 Subject: [PATCH 080/236] Use python code block in library.rst --- docs/library.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/library.rst b/docs/library.rst index 032b5c8..263d463 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -16,7 +16,7 @@ Genomic Position Model Genomic positions follow the HGVS genomic coordinate system. They are represented as 1-key dictionaries. Below is an example of `g.1` in HGVS. -.. code-block:: json +.. code-block:: python {'position':1} From ca45f679b380a4b658d1896532eddd93a766bc7e Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:17:47 +0100 Subject: [PATCH 081/236] Update library.rst --- docs/library.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 263d463..5d84c9e 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -58,7 +58,7 @@ NonCoding Position Model Noncoding positions follow the HGVS ``n`` coordinate system. They are represented as 3-key dictionaries. Below is an example of ``n.14+1`` in HGVS. -.. code-block:: json +.. code-block:: python { 'position': 14, @@ -151,7 +151,7 @@ Coding Position Model Coding positions follow the HGVS ``c`` coordinate system. They are represented as 3-key dictionaries. Here is an example of ``c.*1+3``. -.. code-block:: json +.. code-block:: python { 'position': 1, @@ -228,7 +228,7 @@ Additionally, the functions ``coordinate_to_protein()`` and ``protein_to_coordinate()`` can be used. These functions use a 4-key dictionary to represent a protein position. Here is an example of ``p.1`` in HGVS. -.. code-block:: json +.. code-block:: python { 'position': 1, From e4e1642cd379323feba882e4ac75ecfdb7ed80de Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:32:24 +0100 Subject: [PATCH 082/236] Update library.rst --- docs/library.rst | 244 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 200 insertions(+), 44 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 5d84c9e..1f5ac9a 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -124,18 +124,56 @@ parameter should be set to ``True``. In our example, HGVS position "g.36" 35 In the following table, we show a number of annotated examples. -.. csv-table:: - :class: table-scroll - :header: "Coordinate", "Position", "Offset", "Region", "HGVS" - - "0", "5", "0", `u`, `n.u5` - "4", "1", "0", `u`, `n.u1` - "5", "1", "0", `""`, `n.1` - "24", "9", "5", `""`, `n.9+5` - "25", "10", "-5", `""`, `n.10-5` - "71", "22", "0", `""`, `n.22` - "72", "1", "0", `d`, `n.d1` - "79", "8", "0", `d`, `n.d8` + +.. _table_noncoding: +.. list-table:: Coordinates to Noncoding Positions mapping. + :header-rows: 1 + + * - Coordinate + - Position + - Offset + - Region + - HGVS + * - 0 + - 5 + - 0 + - ``u`` + - ``n.u5`` + * - 4 + - 1 + - 0 + - ``u`` + - ``n.u1`` + * - 5 + - 1 + - 0 + - `` + - ``n.1`` + * - 24 + - 9 + - 5 + - ```` + - ``n.9+5`` + * - 25 + - 10 + - -5 + - ```` + - ``n.10-5`` + * - 71 + - 22 + - 0 + - ```` + - ``n.22`` + * - 72 + - 1 + - 0 + - ``d`` + - ``n.d1`` + * - 79 + - 8 + - 0 + - ``d`` + - ``n.d8`` See section :doc:`api/crossmap` for a detailed description. @@ -203,22 +241,75 @@ longer described using the ``'u'`` or ``'d'`` notation. In the following table, we show a number of annotated examples. -.. csv-table:: - :class: - :header: "Coordinate", "Position", "Offset", "Region", "HGVS" - - "0", "5", "0", `u`, `c.u5` - "4", "1", "0", `u`, `c.u1` - "5", "11", "0", `-`, `c.-11` - "24", "3", "5", `-`, `c.-3+5` - "31", "1", "0", `-`, `c.-1` - "32", "1", "0", `""`, `c.1` - "37", "3", "3", `""`, `c.3+3` - "38", "4", "-2", `""`, `c.4-2` - "43", "1", "0", `*`, `c.*1` - "61", "4", "-9", `*`, `c.*4+9` - "71", "5", "0", `*`, `c.*5` - "79", "8", "0", `d`, `c.d8` +.. _table_coding: +.. list-table:: Coordinates to Coding Positions mapping + :header-rows: 1 + + * - Coordinate + - Position + - Offset + - Region + - HGVS + * - 0 + - 5 + - 0 + - ``u`` + - ``c.u5`` + * - 4 + - 1 + - 0 + - ``u`` + - ``c.u1`` + * - 5 + - 11 + - 0 + - ``-`` + - ``c.-11`` + * - 24 + - 3 + - 5 + - ``-`` + - ``c.-3+5`` + * - 31 + - 1 + - 0 + - ``-`` + - ``c.-1`` + * - 32 + - 1 + - 0 + - `` + - ``c.1`` + * - 37 + - 3 + - 3 + - `` + - ``c.3+3`` + * - 38 + - 4 + - -2 + - `` + - ``c.4-2`` + * - 43 + - 1 + - 0 + - ``*`` + - ``c.*1`` + * - 61 + - 4 + - -9 + - ``*`` + - ``c.*4+9`` + * - 71 + - 5 + - 0 + - ``*`` + - ``c.*5`` + * - 79 + - 8 + - 0 + - ``d`` + - ``c.d8`` Protein @@ -259,22 +350,87 @@ Note that the protein position only corresponds with the HGVS "p." notation when the offset equals ``0`` and the region equals ``1``. In the following table, we show a number of annotated examples. -.. csv-table:: - :class: table-scroll - :header: "Coordinate", "Position", "position_in_codon", "Offset", "Region", "HGVS" - - "0", "4", "2", "0", `u`, `` - "4", "4", "2", "0", `u`, `` - "5", "4", "2", "0", `-`, `` - "6", "4", "3", "0", `-`, `` - "7", "3", "1", "0", `-`, `` - "31", "1", "3", "0", `-`, `` - "32", "1", "1", "0", ``, `p.1` - "33", "1", "2", "0", ``, `p.1` - "42", "2", "3", "0", ``, `p.2` - "43", "1", "1", "0", `*`, `` - "44", "1", "1", "1", `*`, `` - "79", "2", "2", "0", `d`, `` +.. list-table::Coordinates to Protein Positions mapping + :header-rows: 1 + + * - Coordinate + - Position + - position_in_codon + - Offset + - Region + - HGVS + * - 0 + - 4 + - 2 + - 0 + - ``u`` + - `` + * - 4 + - 4 + - 2 + - 0 + - ``u`` + - `` + * - 5 + - 4 + - 2 + - 0 + - ``-`` + - `` + * - 6 + - 4 + - 3 + - 0 + - ``-`` + - `` + * - 7 + - 3 + - 1 + - 0 + - ``-`` + - `` + * - 31 + - 1 + - 3 + - 0 + - ``-`` + - `` + * - 32 + - 1 + - 1 + - 0 + - `` + - ``p.1`` + * - 33 + - 1 + - 2 + - 0 + - `` + - ``p.1`` + * - 42 + - 2 + - 3 + - 0 + - `` + - ``p.2`` + * - 43 + - 1 + - 1 + - 0 + - ``*`` + - `` + * - 44 + - 1 + - 1 + - 1 + - ``*`` + - `` + * - 79 + - 2 + - 2 + - 0 + - ``d`` + - `` From b630851a155488efbd369bf7b0ec942b9007404d Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:38:56 +0100 Subject: [PATCH 083/236] Update library.rst --- docs/library.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 1f5ac9a..6d103a9 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -96,7 +96,7 @@ position "n.14+1". We can convert between these two as follows. >>> crossmap.noncoding_to_coordinate({'position':14, 'offset':1, 'region':''}) {'position':14, 'offset':1, 'region':''} -When the coordinate is upstream or downstream of the transcript, we use ``'u`` to +When the coordinate is upstream or downstream of the transcript, we use ``'u'`` to present upstream and ``'d'`` to present downstream. .. code:: python @@ -152,17 +152,17 @@ In the following table, we show a number of annotated examples. * - 24 - 9 - 5 - - ```` + - `` - ``n.9+5`` * - 25 - 10 - -5 - - ```` + - `` - ``n.10-5`` * - 71 - 22 - 0 - - ```` + - `` - ``n.22`` * - 72 - 1 From a0514c8ad17b2de550272be1e9bdd4e70cb8e56b Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:44:22 +0100 Subject: [PATCH 084/236] Update library.rst --- docs/library.rst | 132 +++++++++++++++++++++++------------------------ 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 6d103a9..4e84d7e 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -137,43 +137,43 @@ In the following table, we show a number of annotated examples. * - 0 - 5 - 0 - - ``u`` - - ``n.u5`` + - u`` + - n.u5`` * - 4 - 1 - 0 - - ``u`` - - ``n.u1`` + - u`` + - n.u1`` * - 5 - 1 - 0 - - `` - - ``n.1`` + - + - n.1`` * - 24 - 9 - 5 - - `` - - ``n.9+5`` + - + - n.9+5`` * - 25 - 10 - -5 - - `` - - ``n.10-5`` + - + - n.10-5`` * - 71 - 22 - 0 - - `` - - ``n.22`` + - + - n.22`` * - 72 - 1 - 0 - - ``d`` - - ``n.d1`` + - d`` + - n.d1`` * - 79 - 8 - 0 - - ``d`` - - ``n.d8`` + - d`` + - n.d8`` See section :doc:`api/crossmap` for a detailed description. @@ -253,63 +253,63 @@ In the following table, we show a number of annotated examples. * - 0 - 5 - 0 - - ``u`` - - ``c.u5`` + - u`` + - c.u5`` * - 4 - 1 - 0 - - ``u`` - - ``c.u1`` + - u`` + - c.u1`` * - 5 - 11 - 0 - - ``-`` - - ``c.-11`` + - -`` + - c.-11`` * - 24 - 3 - 5 - - ``-`` - - ``c.-3+5`` + - -`` + - c.-3+5`` * - 31 - 1 - 0 - - ``-`` - - ``c.-1`` + - -`` + - c.-1`` * - 32 - 1 - 0 - - `` - - ``c.1`` + - + - c.1`` * - 37 - 3 - 3 - - `` - - ``c.3+3`` + - + - c.3+3`` * - 38 - 4 - -2 - - `` - - ``c.4-2`` + - + - c.4-2`` * - 43 - 1 - 0 - - ``*`` - - ``c.*1`` + - *`` + - c.*1`` * - 61 - 4 - -9 - - ``*`` - - ``c.*4+9`` + - *`` + - c.*4+9`` * - 71 - 5 - 0 - - ``*`` - - ``c.*5`` + - *`` + - c.*5`` * - 79 - 8 - 0 - - ``d`` - - ``c.d8`` + - d`` + - c.d8`` Protein @@ -343,14 +343,14 @@ position "p.2". We can convert between these to as follows. >>> crossmap.coordinate_to_protein(41) {'position':2, 'position_in_codon':2, 'offset':0, 'region':''} - >>> crossmap.protein_to_coordinate({'position':2, 'position_in_codon':2, offset':0, 'region':''}) + >>> crossmap.protein_to_coordinate({'position':2, 'position_in_codon':2, 'offset':0, 'region':''}) 41 Note that the protein position only corresponds with the HGVS "p." notation when the offset equals ``0`` and the region equals ``1``. In the following table, we show a number of annotated examples. -.. list-table::Coordinates to Protein Positions mapping +.. list-table:: Coordinates to Protein Positions mapping :header-rows: 1 * - Coordinate @@ -363,74 +363,74 @@ table, we show a number of annotated examples. - 4 - 2 - 0 - - ``u`` - - `` + - u`` + - * - 4 - 4 - 2 - 0 - - ``u`` - - `` + - u`` + - * - 5 - 4 - 2 - 0 - - ``-`` - - `` + - -`` + - * - 6 - 4 - 3 - 0 - - ``-`` - - `` + - -`` + - * - 7 - 3 - 1 - 0 - - ``-`` - - `` + - -`` + - * - 31 - 1 - 3 - 0 - - ``-`` - - `` + - -`` + - * - 32 - 1 - 1 - 0 - - `` - - ``p.1`` + - + - p.1`` * - 33 - 1 - 2 - 0 - - `` - - ``p.1`` + - + - p.1`` * - 42 - 2 - 3 - 0 - - `` - - ``p.2`` + - + - p.2`` * - 43 - 1 - 1 - 0 - - ``*`` - - `` + - *`` + - * - 44 - 1 - 1 - 1 - - ``*`` - - `` + - *`` + - * - 79 - 2 - 2 - 0 - - ``d`` - - `` + - d`` + - From 21ce8dd377781aa13fad3edb238c9f1b853d6f28 Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:50:25 +0100 Subject: [PATCH 085/236] Update library.rst --- docs/library.rst | 84 ++++++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 4e84d7e..4884f84 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -137,23 +137,23 @@ In the following table, we show a number of annotated examples. * - 0 - 5 - 0 - - u`` - - n.u5`` + - ``u`` + - ``n.u5`` * - 4 - 1 - 0 - - u`` - - n.u1`` + - ``u`` + - ``n.u1`` * - 5 - 1 - 0 - - - n.1`` + - ``n.1`` * - 24 - 9 - 5 - - - n.9+5`` + - ``n.9+5`` * - 25 - 10 - -5 @@ -163,17 +163,17 @@ In the following table, we show a number of annotated examples. - 22 - 0 - - - n.22`` + - ``n.22`` * - 72 - 1 - 0 - - d`` - - n.d1`` + - ``d`` + - ``n.d1`` * - 79 - 8 - 0 - - d`` - - n.d8`` + - ``d`` + - ``n.d8`` See section :doc:`api/crossmap` for a detailed description. @@ -253,43 +253,43 @@ In the following table, we show a number of annotated examples. * - 0 - 5 - 0 - - u`` - - c.u5`` + - ``u`` + - ``c.u5`` * - 4 - 1 - 0 - - u`` - - c.u1`` + - ``u`` + - ``c.u1`` * - 5 - 11 - 0 - - -`` - - c.-11`` + - ``-`` + - ``c.-11`` * - 24 - 3 - 5 - - -`` - - c.-3+5`` + - ``-`` + - ``c.-3+5`` * - 31 - 1 - 0 - - -`` - - c.-1`` + - ``-`` + - ``c.-1`` * - 32 - 1 - 0 - - - c.1`` + - ``c.1`` * - 37 - 3 - 3 - - - c.3+3`` + - ``c.3+3`` * - 38 - 4 - -2 - - - c.4-2`` + - ``c.4-2`` * - 43 - 1 - 0 @@ -298,18 +298,18 @@ In the following table, we show a number of annotated examples. * - 61 - 4 - -9 - - *`` - - c.*4+9`` + - ``*`` + - ``c.*4+9`` * - 71 - 5 - 0 - - *`` - - c.*5`` + - ``*`` + - ``c.*5`` * - 79 - 8 - 0 - - d`` - - c.d8`` + - ``d`` + - ``c.d8`` Protein @@ -363,73 +363,73 @@ table, we show a number of annotated examples. - 4 - 2 - 0 - - u`` + - ``u`` - * - 4 - 4 - 2 - 0 - - u`` + - ``u`` - * - 5 - 4 - 2 - 0 - - -`` + - ``-`` - * - 6 - 4 - 3 - 0 - - -`` + - ``-`` - * - 7 - 3 - 1 - 0 - - -`` + - ``-`` - * - 31 - 1 - 3 - 0 - - -`` + - ``-`` - * - 32 - 1 - 1 - 0 - - - p.1`` + - ``p.1`` * - 33 - 1 - 2 - 0 - - - p.1`` + - ``p.1`` * - 42 - 2 - 3 - 0 - - - p.2`` + - ``p.2`` * - 43 - 1 - 1 - 0 - - *`` + - ``*`` - * - 44 - 1 - 1 - 1 - - *`` + - ``*`` - * - 79 - 2 - 2 - 0 - - d`` + - ``d`` - From 6fc7709b76452125225decc66f463879d79d7b0b Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:59:11 +0100 Subject: [PATCH 086/236] Update library.rst --- docs/library.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 4884f84..ab46719 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -158,7 +158,7 @@ In the following table, we show a number of annotated examples. - 10 - -5 - - - n.10-5`` + - ``n.10-5`` * - 71 - 22 - 0 @@ -293,8 +293,8 @@ In the following table, we show a number of annotated examples. * - 43 - 1 - 0 - - *`` - - c.*1`` + - ``*`` + - ``c.*1`` * - 61 - 4 - -9 @@ -350,6 +350,7 @@ Note that the protein position only corresponds with the HGVS "p." notation when the offset equals ``0`` and the region equals ``1``. In the following table, we show a number of annotated examples. +.. _table_protein: .. list-table:: Coordinates to Protein Positions mapping :header-rows: 1 From 1693bab2ede6ade8964a9e9e6c0ca2414ed44fd0 Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:43:35 +0100 Subject: [PATCH 087/236] Update library.rst --- docs/library.rst | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index ab46719..102dbb2 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -147,22 +147,22 @@ In the following table, we show a number of annotated examples. * - 5 - 1 - 0 - - + - - ``n.1`` * - 24 - 9 - 5 - - + - - ``n.9+5`` * - 25 - 10 - -5 - - + - - ``n.10-5`` * - 71 - 22 - 0 - - + - - ``n.22`` * - 72 - 1 @@ -278,17 +278,17 @@ In the following table, we show a number of annotated examples. * - 32 - 1 - 0 - - + - - ``c.1`` * - 37 - 3 - 3 - - + - - ``c.3+3`` * - 38 - 4 - -2 - - + - - ``c.4-2`` * - 43 - 1 @@ -365,74 +365,73 @@ table, we show a number of annotated examples. - 2 - 0 - ``u`` - - + - * - 4 - 4 - 2 - 0 - ``u`` - - + - * - 5 - 4 - 2 - 0 - ``-`` - - + - * - 6 - 4 - 3 - 0 - ``-`` - - + - * - 7 - 3 - 1 - 0 - ``-`` - - + - * - 31 - 1 - 3 - 0 - ``-`` - - + - * - 32 - 1 - 1 - 0 - - + - - ``p.1`` * - 33 - 1 - 2 - 0 - - + - - ``p.1`` * - 42 - 2 - 3 - 0 - - + - - ``p.2`` * - 43 - 1 - 1 - 0 - ``*`` - - + - * - 44 - 1 - 1 - 1 - ``*`` - - + - * - 79 - 2 - 2 - 0 - ``d`` - - - + - See section :doc:`api/crossmap` for a detailed description. @@ -471,7 +470,7 @@ The ``Coding`` class makes use of a number of basic classes described in this section. The ``Locus`` class -^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~ The ``Locus`` class is used to deal with offsets with respect to a single locus. From 9bb4501e04a1c5025f2fdba6e4e59cca7746be8d Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 20 Mar 2026 15:56:55 +0100 Subject: [PATCH 088/236] Add backticks --- docs/library.rst | 278 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 217 insertions(+), 61 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 032b5c8..67bbc75 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -14,9 +14,9 @@ Genomic Position Model ~~~~~~~~~~~~~~~~~~~~~~~ Genomic positions follow the HGVS genomic coordinate system. -They are represented as 1-key dictionaries. Below is an example of `g.1` in HGVS. +They are represented as 1-key dictionaries. Below is an example of ``g.1`` in HGVS. -.. code-block:: json +.. code-block:: python {'position':1} @@ -58,7 +58,7 @@ NonCoding Position Model Noncoding positions follow the HGVS ``n`` coordinate system. They are represented as 3-key dictionaries. Below is an example of ``n.14+1`` in HGVS. -.. code-block:: json +.. code-block:: python { 'position': 14, @@ -86,8 +86,8 @@ NonCoding Position Conversion Now the functions ``coordinate_to_noncoding()`` and ``noncoding_to_coordinate()`` can be used. -In our example, the HGVS position "g.36" (coordinate ``35``) is equivalent to -position "n.14+1". We can convert between these two as follows. +In our example, the HGVS position ``g.36`` (coordinate `35`) is equivalent to +position ``n.14+1``. We can convert between these two as follows. .. code:: python @@ -96,7 +96,7 @@ position "n.14+1". We can convert between these two as follows. >>> crossmap.noncoding_to_coordinate({'position':14, 'offset':1, 'region':''}) {'position':14, 'offset':1, 'region':''} -When the coordinate is upstream or downstream of the transcript, we use ``'u`` to +When the coordinate is upstream or downstream of the transcript, we use ``'u'`` to present upstream and ``'d'`` to present downstream. .. code:: python @@ -112,8 +112,8 @@ present upstream and ``'d'`` to present downstream. For transcripts that reside on the reverse complement strand, the ``inverted`` -parameter should be set to ``True``. In our example, HGVS position "g.36" -(coordinate ``35``) is now equivalent to position "n.9-1". +parameter should be set to ``True``. In our example, HGVS position ``g.36`` +(coordinate `35`) is now equivalent to position ``n.9-1``. .. code:: python @@ -124,18 +124,56 @@ parameter should be set to ``True``. In our example, HGVS position "g.36" 35 In the following table, we show a number of annotated examples. -.. csv-table:: - :class: table-scroll - :header: "Coordinate", "Position", "Offset", "Region", "HGVS" - - "0", "5", "0", `u`, `n.u5` - "4", "1", "0", `u`, `n.u1` - "5", "1", "0", `""`, `n.1` - "24", "9", "5", `""`, `n.9+5` - "25", "10", "-5", `""`, `n.10-5` - "71", "22", "0", `""`, `n.22` - "72", "1", "0", `d`, `n.d1` - "79", "8", "0", `d`, `n.d8` + +.. _table_noncoding: +.. list-table:: Coordinates to Noncoding Positions mapping. + :header-rows: 1 + + * - coordinate + - position + - offset + - region + - HGVS + * - 0 + - 5 + - 0 + - ``u`` + - ``n.u5`` + * - 4 + - 1 + - 0 + - ``u`` + - ``n.u1`` + * - 5 + - 1 + - 0 + - + - ``n.1`` + * - 24 + - 9 + - 5 + - + - ``n.9+5`` + * - 25 + - 10 + - -5 + - + - ``n.10-5`` + * - 71 + - 22 + - 0 + - + - ``n.22`` + * - 72 + - 1 + - 0 + - ``d`` + - ``n.d1`` + * - 79 + - 8 + - 0 + - ``d`` + - ``n.d8`` See section :doc:`api/crossmap` for a detailed description. @@ -151,7 +189,7 @@ Coding Position Model Coding positions follow the HGVS ``c`` coordinate system. They are represented as 3-key dictionaries. Here is an example of ``c.*1+3``. -.. code-block:: json +.. code-block:: python { 'position': 1, @@ -180,8 +218,8 @@ On top of the functionality provided by the ``NonCoding`` class, the functions ``coordinate_to_coding()`` and ``coding_to_coordinate()`` can be used. These functions use a 3-key dictionary to represent a coding position. -In our example, the HGVS position "g.32" (coordinate ``31``) is equivalent to -position "c.-1". We can convert between these two as follows. +In our example, the HGVS position ``g.32`` (coordinate `31`) is equivalent to +position ``c.-1``. We can convert between these two as follows. .. code:: python @@ -203,22 +241,75 @@ longer described using the ``'u'`` or ``'d'`` notation. In the following table, we show a number of annotated examples. -.. csv-table:: - :class: - :header: "Coordinate", "Position", "Offset", "Region", "HGVS" - - "0", "5", "0", `u`, `c.u5` - "4", "1", "0", `u`, `c.u1` - "5", "11", "0", `-`, `c.-11` - "24", "3", "5", `-`, `c.-3+5` - "31", "1", "0", `-`, `c.-1` - "32", "1", "0", `""`, `c.1` - "37", "3", "3", `""`, `c.3+3` - "38", "4", "-2", `""`, `c.4-2` - "43", "1", "0", `*`, `c.*1` - "61", "4", "-9", `*`, `c.*4+9` - "71", "5", "0", `*`, `c.*5` - "79", "8", "0", `d`, `c.d8` +.. _table_coding: +.. list-table:: Coordinates to Coding Positions mapping + :header-rows: 1 + + * - coordinate + - position + - offset + - region + - HGVS + * - 0 + - 5 + - 0 + - ``u`` + - ``c.u5`` + * - 4 + - 1 + - 0 + - ``u`` + - ``c.u1`` + * - 5 + - 11 + - 0 + - ``-`` + - ``c.-11`` + * - 24 + - 3 + - 5 + - ``-`` + - ``c.-3+5`` + * - 31 + - 1 + - 0 + - ``-`` + - ``c.-1`` + * - 32 + - 1 + - 0 + - + - ``c.1`` + * - 37 + - 3 + - 3 + - + - ``c.3+3`` + * - 38 + - 4 + - -2 + - + - ``c.4-2`` + * - 43 + - 1 + - 0 + - ``*`` + - ``c.*1`` + * - 61 + - 4 + - -9 + - ``*`` + - ``c.*4+9`` + * - 71 + - 5 + - 0 + - ``*`` + - ``c.*5`` + * - 79 + - 8 + - 0 + - ``d`` + - ``c.d8`` Protein @@ -228,7 +319,7 @@ Additionally, the functions ``coordinate_to_protein()`` and ``protein_to_coordinate()`` can be used. These functions use a 4-key dictionary to represent a protein position. Here is an example of ``p.1`` in HGVS. -.. code-block:: json +.. code-block:: python { 'position': 1, @@ -245,37 +336,102 @@ Where: - **region**: a string describing the region type (``''`` for standard positions) -In our example the HGVS position "g.42" (coordinate ``41``) corresponds with -position "p.2". We can convert between these to as follows. +In our example the HGVS position ``g.42`` (coordinate `41`) corresponds with +position ``p.2``. We can convert between these to as follows. .. code:: python >>> crossmap.coordinate_to_protein(41) {'position':2, 'position_in_codon':2, 'offset':0, 'region':''} - >>> crossmap.protein_to_coordinate({'position':2, 'position_in_codon':2, offset':0, 'region':''}) + >>> crossmap.protein_to_coordinate({'position':2, 'position_in_codon':2, 'offset':0, 'region':''}) 41 Note that the protein position only corresponds with the HGVS "p." notation when the offset equals ``0`` and the region equals ``1``. In the following table, we show a number of annotated examples. -.. csv-table:: - :class: table-scroll - :header: "Coordinate", "Position", "position_in_codon", "Offset", "Region", "HGVS" - - "0", "4", "2", "0", `u`, `` - "4", "4", "2", "0", `u`, `` - "5", "4", "2", "0", `-`, `` - "6", "4", "3", "0", `-`, `` - "7", "3", "1", "0", `-`, `` - "31", "1", "3", "0", `-`, `` - "32", "1", "1", "0", ``, `p.1` - "33", "1", "2", "0", ``, `p.1` - "42", "2", "3", "0", ``, `p.2` - "43", "1", "1", "0", `*`, `` - "44", "1", "1", "1", `*`, `` - "79", "2", "2", "0", `d`, `` - +.. _table_protein: +.. list-table:: Coordinates to Protein Positions mapping + :header-rows: 1 + + * - coordinate + - position + - position_in_codon + - offset + - region + - HGVS + * - 0 + - 4 + - 2 + - 0 + - ``u`` + - + * - 4 + - 4 + - 2 + - 0 + - ``u`` + - + * - 5 + - 4 + - 2 + - 0 + - ``-`` + - + * - 6 + - 4 + - 3 + - 0 + - ``-`` + - + * - 7 + - 3 + - 1 + - 0 + - ``-`` + - + * - 31 + - 1 + - 3 + - 0 + - ``-`` + - + * - 32 + - 1 + - 1 + - 0 + - + - ``p.1`` + * - 33 + - 1 + - 2 + - 0 + - + - ``p.1`` + * - 42 + - 2 + - 3 + - 0 + - + - ``p.2`` + * - 43 + - 1 + - 1 + - 0 + - ``*`` + - + * - 44 + - 1 + - 1 + - 1 + - ``*`` + - + * - 79 + - 2 + - 2 + - 0 + - ``d`` + - See section :doc:`api/crossmap` for a detailed description. @@ -314,7 +470,7 @@ The ``Coding`` class makes use of a number of basic classes described in this section. The ``Locus`` class -^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~ The ``Locus`` class is used to deal with offsets with respect to a single locus. From 691ec354aaad6e7b0ee8d63a826ead3bdf9bbe6d Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 20 Mar 2026 17:01:30 +0100 Subject: [PATCH 089/236] Cleanup --- docs/library.rst | 66 +++++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 67bbc75..a1c236e 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -8,7 +8,7 @@ The ``Genomic`` class --------------------- The ``Genomic`` class provides an interface to conversions between genomic -(``g.``, ``m``, ``n``) positions and coordinates. +(``g.``, ``m.``, ``n.``) positions and coordinates. Genomic Position Model ~~~~~~~~~~~~~~~~~~~~~~~ @@ -22,7 +22,7 @@ They are represented as 1-key dictionaries. Below is an example of ``g.1`` in HG Where: -- **position**: a positive integer(>0) +- **position**: a positive integer repersenting a base position(>0) Genomic Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -55,7 +55,7 @@ systems should be done via a coordinate. NonCoding Position Model ~~~~~~~~~~~~~~~~~~~~~~~~ -Noncoding positions follow the HGVS ``n`` coordinate system. They are represented +Noncoding positions follow the HGVS ``n.`` coordinate system. They are represented as 3-key dictionaries. Below is an example of ``n.14+1`` in HGVS. .. code-block:: python @@ -200,9 +200,10 @@ represented as 3-key dictionaries. Here is an example of ``c.*1+3``. Where: - **position**: an interger representing a transcript position (>0) -- **offset**: an integer indicating the offset relative to the position -- **region**: a string describing the region type (`''` for standard coding positions, - `'-'` for 5' UTR, `'*'` for 3' UTR, `'u'` for upstream and ``'d'`` for downstream) +- **offset**: an integer indicating the offset relative to the position (negative for upstream, + positive for downstream) +- **region**: a string describing the region type (``''`` for standard coding positions, + ``'-'`` for 5' UTR, ``'*'`` for 3' UTR, ``'u'`` for upstream and ``'d'`` for downstream) Coding Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -230,7 +231,8 @@ position ``c.-1``. We can convert between these two as follows. The ``coordinate_to_coding()`` function accepts an optional ``degenerate`` argument. When set to ``True``, positions outside of the transcript are no -longer described using the ``'u'`` or ``'d'`` notation. +longer described using the ``'u'`` or ``'d'`` notation, ``'-'`` and ``'*'`` +are used instead. .. code:: python @@ -299,7 +301,7 @@ In the following table, we show a number of annotated examples. - 4 - -9 - ``*`` - - ``c.*4+9`` + - ``c.*4-9`` * - 71 - 5 - 0 @@ -365,37 +367,37 @@ table, we show a number of annotated examples. - 2 - 0 - ``u`` - - + - invalid * - 4 - 4 - 2 - 0 - ``u`` - - + - invalid * - 5 - 4 - 2 - 0 - ``-`` - - - * - 6 - - 4 + - invalid + * - 7 - 3 + - 1 - 0 - ``-`` - - - * - 7 + - invalid + * - 8 - 3 - 1 - - 0 + - 1 - ``-`` - - + - invalid * - 31 - 1 - 3 - 0 - ``-`` - - + - invalid * - 32 - 1 - 1 @@ -419,19 +421,19 @@ table, we show a number of annotated examples. - 1 - 0 - ``*`` - - + - invalid * - 44 - 1 - 1 - 1 - ``*`` - - + - invalid * - 79 - 2 - 2 - 0 - ``d`` - - + - invalid See section :doc:`api/crossmap` for a detailed description. @@ -474,6 +476,9 @@ The ``Locus`` class The ``Locus`` class is used to deal with offsets with respect to a single locus. +**Note:** the ``position`` values in the position dictionaries are **0-based**, +so the first base of the locus corresponds to ``{'position': 0, 'offset': 0}``. +This differs from HGVS numbering, which is **1-based**. .. code:: python @@ -488,7 +493,9 @@ for the semantics. .. code:: python >>> locus.to_position(9) - {'position':1, 'offset':-1} + {'position':0, 'offset':-1} + >>> locus.to_coordinate({'position':0, 'offset':-1}) + {'position':0, 'offset':-1} For loci that reside on the reverse complement strand, the optional ``inverted`` constructor parameter should be set to ``True``. @@ -499,20 +506,27 @@ The ``MultiLocus`` class ^^^^^^^^^^^^^^^^^^^^^^^^ The ``MultiLocus`` class is used to deal with offsets with respect to multiple -loci. +loci. Its positions is .. code:: python >>> from mutalyzer_crossmapper import MultiLocus >>> multilocus = MultiLocus([(10, 20), (40, 50)]) -The interface to this class is similar to that of the ``Locus`` class. +The interface to this class is similar to that of the ``Locus`` class. Functions +``to_position()`` and ``to_coordinate()`` work with a 3-key dictionary. + +**Note:** again, the ``position`` values in the position dictionaries are **0-based**. .. code:: python >>> multilocus.to_position(22) - {'position':10, 'offset':3, 'region':''} + {'position':9, 'offset':3, 'region':''} + >>> multilocus.to_coordinate({'position':9, 'offset':3, 'region':''}) + 22 >>> multilocus.to_position(38) - {'position':11, 'offset':-2, 'region':''} + {'position':10, 'offset':-2, 'region':''} + >>> multilocus.to_coordinate({'position':10, 'offset':-2, 'region':''} + 38 See section :doc:`api/multi_locus` for a detailed description. From bcf7bd21fe0a0a2a98f123db351417f9ffb48aed Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 20 Mar 2026 17:09:01 +0100 Subject: [PATCH 090/236] Cleanup, use '' in dictionary --- mutalyzer_crossmapper/crossmapper.py | 4 +- mutalyzer_crossmapper/locus.py | 16 +- mutalyzer_crossmapper/multi_locus.py | 28 +-- tests/helper.py | 19 +- tests/test_crossmapper.py | 276 +++++++++++++-------------- tests/test_locus.py | 32 ++-- tests/test_multi_locus.py | 84 ++++---- 7 files changed, 224 insertions(+), 235 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 3994271..e68bbbd 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -130,7 +130,7 @@ def _coordinate_to_coding(self, coordinate): """ noncoding_pos_m = self._noncoding.to_position(coordinate) - if noncoding_pos_m['region'] in ['u', 'd']: + if noncoding_pos_m['region'] in ('u', 'd'): return noncoding_pos_m location = noncoding_pos_m['position'] @@ -178,7 +178,7 @@ def _coding_to_coordinate(self, pos_m): position = pos_m['position'] region = pos_m['region'] - if region in ['u', 'd']: + if region in ('u', 'd'): return self._noncoding.to_coordinate(pos_m) noncoding_pos_m = {'offset': pos_m['offset'], 'region': ''} diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 738e87c..eb23efd 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -19,16 +19,16 @@ def to_position(self, coordinate): """ if self._inverted: if coordinate > self.boundary[1]: - return {"position": 0, "offset": self.boundary[1] - coordinate} + return {'position': 0, 'offset': self.boundary[1] - coordinate} if coordinate < self.boundary[0]: - return {"position": self._end, "offset": self.boundary[0] - coordinate} - return {"position": self.boundary[1] - coordinate, "offset": 0} + return {'position': self._end, 'offset': self.boundary[0] - coordinate} + return {'position': self.boundary[1] - coordinate, 'offset': 0} if coordinate < self.boundary[0]: - return {"position": 0, "offset": coordinate - self.boundary[0]} + return {'position': 0, 'offset': coordinate - self.boundary[0]} if coordinate > self.boundary[1]: - return {"position": self._end, "offset": coordinate - self.boundary[1]} - return {"position": coordinate - self.boundary[0], "offset": 0} + return {'position': self._end, 'offset': coordinate - self.boundary[1]} + return {'position': coordinate - self.boundary[0], 'offset': 0} def to_coordinate(self, pos_m): """Convert a position model to a coordinate. @@ -38,5 +38,5 @@ def to_coordinate(self, pos_m): :returns int: Coordinate. """ if self._inverted: - return self.boundary[1] - pos_m["position"] - pos_m["offset"] - return self.boundary[0] + pos_m["position"] + pos_m["offset"] + return self.boundary[1] - pos_m['position'] - pos_m['offset'] + return self.boundary[0] + pos_m['position'] + pos_m['offset'] diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 861e5cd..c76a39b 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -58,20 +58,20 @@ def to_position(self, coordinate:int): """ index = nearest_location(self._locations, coordinate, self._inverted) outside = self._orientation * self.outside(coordinate) - region = "u" if outside < 0 else "d" if outside > 0 else "" + region = 'u' if outside < 0 else 'd' if outside > 0 else '' location = self._loci[index].to_position(coordinate) if outside: return { - "position": abs(location["offset"]), - "offset": 0, - "region": region + 'position': abs(location['offset']), + 'offset': 0, + 'region': region } return { - "position": location["position"] + self._offsets[self._direction(index)], - "offset": location["offset"], - "region": region + 'position': location['position'] + self._offsets[self._direction(index)], + 'offset': location['offset'], + 'region': region } def to_coordinate(self, pos_m:dict): @@ -81,19 +81,19 @@ def to_coordinate(self, pos_m:dict): :returns int: Coordinate. """ - region = pos_m["region"] + region = pos_m['region'] - if pos_m["region"] in ("u", "d"): - is_upstream = region == "u" + if pos_m['region'] in ('u', 'd'): + is_upstream = region == 'u' if self._inverted: is_upstream = not is_upstream if is_upstream: - return self._locations[0][0] - abs(pos_m["position"]) + pos_m["offset"] - return abs(pos_m["position"]) + self._locations[-1][1] + pos_m["offset"] - 1 + return self._locations[0][0] - abs(pos_m['position']) + pos_m['offset'] + return abs(pos_m['position']) + self._locations[-1][1] + pos_m['offset'] - 1 index = min( len(self._offsets), - max(0, bisect_right(self._offsets, pos_m["position"]) - 1) + max(0, bisect_right(self._offsets, pos_m['position']) - 1) ) - locus_pos_m = {**pos_m, "position": pos_m["position"] - self._offsets[index]} + locus_pos_m = {**pos_m, 'position': pos_m['position'] - self._offsets[index]} return self._loci[self._direction(index)].to_coordinate(locus_pos_m) diff --git a/tests/helper.py b/tests/helper.py index 9e51367..b3ca042 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -2,19 +2,8 @@ def invariant(f, x, f_i, y): assert f(x) == y assert f_i(y) == x -def degenerate_equal(f, coordinate, locations): - results = [f(loc) for loc in locations] - - # First condition: first maps correctly - assert results[0] == coordinate, ( - f"\nFirst location: {locations[0]}" - f"\nExpected: {coordinate}" - f"\nGot: {results[0]}" - ) - # Second condition: all map to same coordinate - assert len(set(results)) == 1, ( - f"\nLocations: {locations}" - f"\nResults: {results}" - f"\nExpected all to map to the same coordinate" - ) \ No newline at end of file +def degenerate_equal(f, coordinate, locations): + assert f(locations[0]) == coordinate + assert len( + set(map(f, locations))) == 1 \ No newline at end of file diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index a3bc5f7..97c6262 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -14,13 +14,13 @@ def test_Genomic(): crossmap.coordinate_to_genomic, 0, crossmap.genomic_to_coordinate, - {"position": 1}, + {'position': 1}, ) invariant( crossmap.coordinate_to_genomic, 98, crossmap.genomic_to_coordinate, - {"position": 99}, + {'position': 99}, ) @@ -33,19 +33,19 @@ def test_NonCoding(): crossmap.coordinate_to_noncoding, 3, crossmap.noncoding_to_coordinate, - {"position": 2, "offset": 0, "region": "u"}, + {'position': 2, 'offset': 0, 'region': 'u'}, ) invariant( crossmap.coordinate_to_noncoding, 4, crossmap.noncoding_to_coordinate, - {"position": 1, "offset": 0, "region": "u"}, + {'position': 1, 'offset': 0, 'region': 'u'}, ) invariant( crossmap.coordinate_to_noncoding, 5, crossmap.noncoding_to_coordinate, - {"position": 1, "offset": 0, "region": ""}, + {'position': 1, 'offset': 0, 'region': ''}, ) # Boundary between downstream and transcript. @@ -53,13 +53,13 @@ def test_NonCoding(): crossmap.coordinate_to_noncoding, 71, crossmap.noncoding_to_coordinate, - {"position": 22, "offset": 0, "region": ""}, + {'position': 22, 'offset': 0, 'region': ''}, ) invariant( crossmap.coordinate_to_noncoding, 72, crossmap.noncoding_to_coordinate, - {"position": 1, "offset": 0, "region": "d"}, + {'position': 1, 'offset': 0, 'region': 'd'}, ) @@ -72,13 +72,13 @@ def test_NonCoding_inverted(): crossmap.coordinate_to_noncoding, 72, crossmap.noncoding_to_coordinate, - {"position": 1, "offset": 0, "region": "u"}, + {'position': 1, 'offset': 0, 'region': 'u'}, ) invariant( crossmap.coordinate_to_noncoding, 71, crossmap.noncoding_to_coordinate, - {"position": 1, "offset": 0, "region": ""}, + {'position': 1, 'offset': 0, 'region': ''}, ) # Boundary between downstream and transcript. @@ -86,13 +86,13 @@ def test_NonCoding_inverted(): crossmap.coordinate_to_noncoding, 5, crossmap.noncoding_to_coordinate, - {"position": 22, "offset": 0, "region": ""}, + {'position': 22, 'offset': 0, 'region': ''}, ) invariant( crossmap.coordinate_to_noncoding, 4, crossmap.noncoding_to_coordinate, - {"position": 1, "offset": 0, "region": "d"}, + {'position': 1, 'offset': 0, 'region': 'd'}, ) @@ -105,8 +105,8 @@ def test_NonCoding_degenerate(): crossmap.noncoding_to_coordinate, 4, [ - {"position": 1, "offset": -1, "region": ""}, - {"position": 1, "offset": 0, "region": "u"}, + {'position': 1, 'offset': -1, 'region': ''}, + {'position': 1, 'offset': 0, 'region': 'u'}, ], ) @@ -115,9 +115,9 @@ def test_NonCoding_degenerate(): crossmap.noncoding_to_coordinate, 72, [ - {"position": 1, "offset": 0, "region": "d"}, - {"position": 22, "offset": 1, "region": ""}, - {"position": 23, "offset": 0, "region": ""}, + {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 22, 'offset': 1, 'region': ''}, + {'position': 23, 'offset': 0, 'region': ''}, ], ) @@ -131,8 +131,8 @@ def test_NonCoding_inverted_degenerate(): crossmap.noncoding_to_coordinate, 72, [ - {"position": 1, "offset": -1, "region": ""}, - {"position": 1, "offset": 0, "region": "u"}, + {'position': 1, 'offset': -1, 'region': ''}, + {'position': 1, 'offset': 0, 'region': 'u'}, ], ) @@ -141,9 +141,9 @@ def test_NonCoding_inverted_degenerate(): crossmap.noncoding_to_coordinate, 4, [ - {"position": 1, "offset": 0, "region": "d"}, - {"position": 23, "offset": 0, "region": ""}, - {"position": 22, "offset": 1, "region": ""}, + {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 23, 'offset': 0, 'region': ''}, + {'position': 22, 'offset': 1, 'region': ''}, ], ) @@ -157,13 +157,13 @@ def test_Coding(): crossmap.coordinate_to_coding, 31, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": "-"}, + {'position': 1, 'offset': 0, 'region': '-'}, ) invariant( crossmap.coordinate_to_coding, 32, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": ""}, + {'position': 1, 'offset': 0, 'region': ''}, ) # Boundary between CDS and 3'. @@ -171,13 +171,13 @@ def test_Coding(): crossmap.coordinate_to_coding, 42, crossmap.coding_to_coordinate, - {"position": 6, "offset": 0, "region": ""}, + {'position': 6, 'offset': 0, 'region': ''}, ) invariant( crossmap.coordinate_to_coding, 43, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": "*"}, + {'position': 1, 'offset': 0, 'region': '*'}, ) @@ -190,13 +190,13 @@ def test_Coding_inverted(): crossmap.coordinate_to_coding, 43, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": "-"}, + {'position': 1, 'offset': 0, 'region': '-'}, ) invariant( crossmap.coordinate_to_coding, 42, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": ""}, + {'position': 1, 'offset': 0, 'region': ''}, ) # Boundary between CDS and 3'. @@ -204,13 +204,13 @@ def test_Coding_inverted(): crossmap.coordinate_to_coding, 32, crossmap.coding_to_coordinate, - {"position": 6, "offset": 0, "region": ""}, + {'position': 6, 'offset': 0, 'region': ''}, ) invariant( crossmap.coordinate_to_coding, 31, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": "*"}, + {'position': 1, 'offset': 0, 'region': '*'}, ) @@ -223,13 +223,13 @@ def test_Coding_regions(): crossmap.coordinate_to_coding, 25, crossmap.coding_to_coordinate, - {"position": 1, "offset": 5, "region": "-"}, + {'position': 1, 'offset': 5, 'region': '-'}, ) invariant( crossmap.coordinate_to_coding, 26, crossmap.coding_to_coordinate, - {"position": 1, "offset": -4, "region": ""}, + {'position': 1, 'offset': -4, 'region': ''}, ) # Downstream odd length intron between two regions. @@ -237,13 +237,13 @@ def test_Coding_regions(): crossmap.coordinate_to_coding, 44, crossmap.coding_to_coordinate, - {"position": 10, "offset": 5, "region": ""}, + {'position': 10, 'offset': 5, 'region': ''}, ) invariant( crossmap.coordinate_to_coding, 45, crossmap.coding_to_coordinate, - {"position": 1, "offset": -4, "region": "*"}, + {'position': 1, 'offset': -4, 'region': '*'}, ) @@ -256,13 +256,13 @@ def test_Coding_regions_inverted(): crossmap.coordinate_to_coding, 44, crossmap.coding_to_coordinate, - {"position": 1, "offset": 5, "region": "-"}, + {'position': 1, 'offset': 5, 'region': '-'}, ) invariant( crossmap.coordinate_to_coding, 43, crossmap.coding_to_coordinate, - {"position": 1, "offset": -4, "region": ""}, + {'position': 1, 'offset': -4, 'region': ''}, ) # Downstream odd length intron between two regions. @@ -270,13 +270,13 @@ def test_Coding_regions_inverted(): crossmap.coordinate_to_coding, 25, crossmap.coding_to_coordinate, - {"position": 10, "offset": 5, "region": ""}, + {'position': 10, 'offset': 5, 'region': ''}, ) invariant( crossmap.coordinate_to_coding, 24, crossmap.coding_to_coordinate, - {"position": 1, "offset": -4, "region": "*"}, + {'position': 1, 'offset': -4, 'region': '*'}, ) @@ -289,13 +289,13 @@ def test_Coding_no_utr5(): crossmap.coordinate_to_coding, 9, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": "u"}, + {'position': 1, 'offset': 0, 'region': 'u'}, ) invariant( crossmap.coordinate_to_coding, 10, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": ""}, + {'position': 1, 'offset': 0, 'region': ''}, ) @@ -308,13 +308,13 @@ def test_Coding_no_utr5_inverted(): crossmap.coordinate_to_coding, 20, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": "u"}, + {'position': 1, 'offset': 0, 'region': 'u'}, ) invariant( crossmap.coordinate_to_coding, 19, crossmap.coding_to_coordinate, - {"position": 1 , "offset": 0, "region": ""}, + {'position': 1 , 'offset': 0, 'region': ''}, ) @@ -327,13 +327,13 @@ def test_Coding_no_utr3(): crossmap.coordinate_to_coding, 19, crossmap.coding_to_coordinate, - {"position": 5, "offset": 0, "region": ""}, + {'position': 5, 'offset': 0, 'region': ''}, ) invariant( crossmap.coordinate_to_coding, 20, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": "d"}, + {'position': 1, 'offset': 0, 'region': 'd'}, ) @@ -346,13 +346,13 @@ def test_Coding_no_utr3_inverted(): crossmap.coordinate_to_coding, 10, crossmap.coding_to_coordinate, - {"position": 5, "offset": 0, "region": ""}, + {'position': 5, 'offset': 0, 'region': ''}, ) invariant( crossmap.coordinate_to_coding, 9, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": "d"}, + {'position': 1, 'offset': 0, 'region': 'd'}, ) @@ -365,19 +365,19 @@ def test_Coding_small_utr5(): crossmap.coordinate_to_coding, 9, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": "u"}, + {'position': 1, 'offset': 0, 'region': 'u'}, ) invariant( crossmap.coordinate_to_coding, 10, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": "-"}, + {'position': 1, 'offset': 0, 'region': '-'}, ) invariant( crossmap.coordinate_to_coding, 11, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": ""}, + {'position': 1, 'offset': 0, 'region': ''}, ) @@ -390,19 +390,19 @@ def test_Coding_small_utr5_inverted(): crossmap.coordinate_to_coding, 20, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": "u"}, + {'position': 1, 'offset': 0, 'region': 'u'}, ) invariant( crossmap.coordinate_to_coding, 19, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": "-"}, + {'position': 1, 'offset': 0, 'region': '-'}, ) invariant( crossmap.coordinate_to_coding, 18, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": ""}, + {'position': 1, 'offset': 0, 'region': ''}, ) @@ -415,19 +415,19 @@ def test_Coding_small_utr3(): crossmap.coordinate_to_coding, 18, crossmap.coding_to_coordinate, - {"position": 4, "offset": 0, "region": ""}, + {'position': 4, 'offset': 0, 'region': ''}, ) invariant( crossmap.coordinate_to_coding, 19, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": "*"}, + {'position': 1, 'offset': 0, 'region': '*'}, ) invariant( crossmap.coordinate_to_coding, 20, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": "d"}, + {'position': 1, 'offset': 0, 'region': 'd'}, ) @@ -440,19 +440,19 @@ def test_Coding_small_utr3_inverted(): crossmap.coordinate_to_coding, 11, crossmap.coding_to_coordinate, - {"position": 4, "offset": 0, "region": ""}, + {'position': 4, 'offset': 0, 'region': ''}, ) invariant( crossmap.coordinate_to_coding, 10, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": "*"}, + {'position': 1, 'offset': 0, 'region': '*'}, ) invariant( crossmap.coordinate_to_coding, 9, crossmap.coding_to_coordinate, - {"position": 1, "offset": 0, "region": "d"}, + {'position': 1, 'offset': 0, 'region': 'd'}, ) @@ -464,25 +464,25 @@ def test_Coding_degenerate(): crossmap.coding_to_coordinate, 9, [ - {"position": 1, "offset": 0, "region": "u"}, - {"position": 2, "offset": 0, "region": "-"}, - {"position": 1, "offset": -2, "region": ""}, - {"position": 1, "offset": -10, "region": "*"}, - {"position": 2, "offset": -11, "region": "*"}, - {"position": 3, "offset": 1, "region": "-"}, - {"position": 4, "offset": 2, "region": "-"}, + {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 2, 'offset': 0, 'region': '-'}, + {'position': 1, 'offset': -2, 'region': ''}, + {'position': 1, 'offset': -10, 'region': '*'}, + {'position': 2, 'offset': -11, 'region': '*'}, + {'position': 3, 'offset': 1, 'region': '-'}, + {'position': 4, 'offset': 2, 'region': '-'}, ], ) degenerate_equal( crossmap.coding_to_coordinate, 20, [ - {"position": 1, "offset": 0, "region": "d"}, - {"position": 2, "offset": 0, "region": "*"}, - {"position": 8, "offset": 2, "region": ""}, - {"position": 1, "offset": 10, "region": "-"}, - {"position": 2, "offset": 11, "region": "-"}, - {"position": 7, "offset": 3, "region": ""}, + {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 2, 'offset': 0, 'region': '*'}, + {'position': 8, 'offset': 2, 'region': ''}, + {'position': 1, 'offset': 10, 'region': '-'}, + {'position': 2, 'offset': 11, 'region': '-'}, + {'position': 7, 'offset': 3, 'region': ''}, ], ) @@ -495,24 +495,24 @@ def test_Coding_inverted_degenerate(): crossmap.coding_to_coordinate, 20, [ - {"position": 1, "offset": 0, "region": "u"}, - {"position": 2, "offset": 0, "region": "-"}, - {"position": 1, "offset": -2, "region": ""}, - {"position": 1, "offset": -10, "region": "*"}, - {"position": 1, "offset": -11, "region": "d"}, - {"position": 2, "offset": -3, "region": ""}, + {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 2, 'offset': 0, 'region': '-'}, + {'position': 1, 'offset': -2, 'region': ''}, + {'position': 1, 'offset': -10, 'region': '*'}, + {'position': 1, 'offset': -11, 'region': 'd'}, + {'position': 2, 'offset': -3, 'region': ''}, ], ) degenerate_equal( crossmap.coding_to_coordinate, 9, [ - {"position": 1, "offset": 0, "region": "d"}, - {"position": 2, "offset": 0, "region": "*"}, - {"position": 8, "offset": 2, "region": ""}, - {"position": 1, "offset": 10, "region": "-"}, - {"position": 1, "offset": 11, "region": "u"}, - {"position": 2, "offset": 12, "region": "u"}, + {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 2, 'offset': 0, 'region': '*'}, + {'position': 8, 'offset': 2, 'region': ''}, + {'position': 1, 'offset': 10, 'region': '-'}, + {'position': 1, 'offset': 11, 'region': 'u'}, + {'position': 2, 'offset': 12, 'region': 'u'}, ], ) @@ -523,14 +523,14 @@ def test_Coding_degenerate_return(): crossmap = Coding([(10, 20)], (11, 19)) assert crossmap.coordinate_to_coding(9, True) == { - "position": 2, - "offset": 0, - "region": "-", + 'position': 2, + 'offset': 0, + 'region': '-', } assert crossmap.coordinate_to_coding(20, True) == { - "position": 2, - "offset": 0, - "region": "*", + 'position': 2, + 'offset': 0, + 'region': '*', } @@ -540,14 +540,14 @@ def test_Coding_inverted_degenerate_return(): assert crossmap.coordinate_to_coding(20, True) == { - "position": 2, - "offset": 0, - "region": "-", + 'position': 2, + 'offset': 0, + 'region': '-', } assert crossmap.coordinate_to_coding(9, True) == { - "position": 2, - "offset": 0, - "region": "*", + 'position': 2, + 'offset': 0, + 'region': '*', } @@ -573,22 +573,22 @@ def test_Coding_no_utr_degenerate(): crossmap.coding_to_coordinate, 9, [ - {"position": 1, "offset": 0, "region": "u"}, - {"position": 1, "offset": 0, "region": "-"}, - {"position": 1, "offset": -2, "region": "*"}, - {"position": 1, "offset": -1, "region": ""}, - {"position": 1, "offset": -2, "region": "d"}, + {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': 0, 'region': '-'}, + {'position': 1, 'offset': -2, 'region': '*'}, + {'position': 1, 'offset': -1, 'region': ''}, + {'position': 1, 'offset': -2, 'region': 'd'}, ], ) degenerate_equal( crossmap.coding_to_coordinate, 11, [ - {"position": 1, "offset": 0, "region": "d"}, - {"position": 1, "offset": 0, "region": "*"}, - {"position": 1, "offset": 2, "region": "-"}, - {"position": 1, "offset": 1, "region": ""}, - {"position": 1, "offset": 2, "region": "u"}, + {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 1, 'offset': 0, 'region': '*'}, + {'position': 1, 'offset': 2, 'region': '-'}, + {'position': 1, 'offset': 1, 'region': ''}, + {'position': 1, 'offset': 2, 'region': 'u'}, ], ) @@ -601,22 +601,22 @@ def test_Coding_inverted_no_utr_degenerate(): crossmap.coding_to_coordinate, 11, [ - {"position": 1, "offset": 0, "region": "u"}, - {"position": 1, "offset": 0, "region": "-"}, - {"position": 1, "offset": -2, "region": "*"}, - {"position": 1, "offset": -1, "region": ""}, - {"position": 1, "offset": -2, "region": "d"}, + {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': 0, 'region': '-'}, + {'position': 1, 'offset': -2, 'region': '*'}, + {'position': 1, 'offset': -1, 'region': ''}, + {'position': 1, 'offset': -2, 'region': 'd'}, ], ) degenerate_equal( crossmap.coding_to_coordinate, 9, [ - {"position": 1, "offset": 0, "region": "d"}, - {"position": 1, "offset": 0, "region": "*"}, - {"position": 1, "offset": 2, "region": "-"}, - {"position": 1, "offset": 1, "region": ""}, - {"position": 1, "offset": 2, "region": "u"}, + {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 1, 'offset': 0, 'region': '*'}, + {'position': 1, 'offset': 2, 'region': '-'}, + {'position': 1, 'offset': 1, 'region': ''}, + {'position': 1, 'offset': 2, 'region': 'u'}, ], ) @@ -626,24 +626,24 @@ def test_Coding_no_utr_degenerate_return(): crossmap = Coding([(10, 11)], (10, 11)) assert crossmap.coordinate_to_coding(8, True) == { - "position": 2, - "offset": 0, - "region": "-", + 'position': 2, + 'offset': 0, + 'region': '-', } assert crossmap.coordinate_to_coding(9, True) == { - "position": 1, - "offset": 0, - "region": "-", + 'position': 1, + 'offset': 0, + 'region': '-', } assert crossmap.coordinate_to_coding(11, True) == { - "position": 1, - "offset": 0, - "region": "*", + 'position': 1, + 'offset': 0, + 'region': '*', } assert crossmap.coordinate_to_coding(12, True) == { - "position": 2, - "offset": 0, - "region": "*", + 'position': 2, + 'offset': 0, + 'region': '*', } @@ -652,14 +652,14 @@ def test_Coding_inverted_no_utr_degenerate_return(): crossmap = Coding([(10, 11)], (10, 11), True) assert crossmap.coordinate_to_coding(11, True) == { - "position": 1, - "offset": 0, - "region": "-", + 'position': 1, + 'offset': 0, + 'region': '-', } assert crossmap.coordinate_to_coding(9, True) == { - "position": 1, - "offset": 0, - "region": "*", + 'position': 1, + 'offset': 0, + 'region': '*', } @@ -672,13 +672,13 @@ def test_Coding_protein(): crossmap.coordinate_to_protein, 31, crossmap.protein_to_coordinate, - {"position": 1, "position_in_codon": 3, "offset": 0, "region": "-"}, + {'position': 1, 'position_in_codon': 3, 'offset': 0, 'region': '-'}, ) invariant( crossmap.coordinate_to_protein, 32, crossmap.protein_to_coordinate, - {"position": 1, "position_in_codon": 1, "offset": 0, "region": ""}, + {'position': 1, 'position_in_codon': 1, 'offset': 0, 'region': ''}, ) # Intron boundary. @@ -686,13 +686,13 @@ def test_Coding_protein(): crossmap.coordinate_to_protein, 34, crossmap.protein_to_coordinate, - {"position": 1, "position_in_codon": 3, "offset": 0, "region": ""}, + {'position': 1, 'position_in_codon': 3, 'offset': 0, 'region': ''}, ) invariant( crossmap.coordinate_to_protein, 35, crossmap.protein_to_coordinate, - {"position": 1, "position_in_codon": 3, "offset": 1, "region": ""}, + {'position': 1, 'position_in_codon': 3, 'offset': 1, 'region': ''}, ) # Boundary between CDS and 3' UTR. @@ -700,11 +700,11 @@ def test_Coding_protein(): crossmap.coordinate_to_protein, 42, crossmap.protein_to_coordinate, - {"position": 2, "position_in_codon": 3, "offset": 0, "region": ""}, + {'position': 2, 'position_in_codon': 3, 'offset': 0, 'region': ''}, ) invariant( crossmap.coordinate_to_protein, 43, crossmap.protein_to_coordinate, - {"position": 1, "position_in_codon": 1, "offset": 0, "region": "*"}, + {'position': 1, 'position_in_codon': 1, 'offset': 0, 'region': '*'}, ) diff --git a/tests/test_locus.py b/tests/test_locus.py index b650c1a..535f93d 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -7,37 +7,37 @@ def test_Locus(): """Forward orientent Lovus.""" locus = Locus((30, 35)) - invariant(locus.to_position, 29, locus.to_coordinate, {"position": 0, "offset": -1}) - invariant(locus.to_position, 30, locus.to_coordinate, {"position": 0, "offset": 0}) - invariant(locus.to_position, 31, locus.to_coordinate, {"position": 1, "offset": 0}) - invariant(locus.to_position, 33, locus.to_coordinate, {"position": 3, "offset": 0}) - invariant(locus.to_position, 34, locus.to_coordinate, {"position": 4, "offset": 0}) - invariant(locus.to_position, 35, locus.to_coordinate, {"position": 4, "offset": 1}) + invariant(locus.to_position, 29, locus.to_coordinate, {'position': 0, 'offset': -1}) + invariant(locus.to_position, 30, locus.to_coordinate, {'position': 0, 'offset': 0}) + invariant(locus.to_position, 31, locus.to_coordinate, {'position': 1, 'offset': 0}) + invariant(locus.to_position, 33, locus.to_coordinate, {'position': 3, 'offset': 0}) + invariant(locus.to_position, 34, locus.to_coordinate, {'position': 4, 'offset': 0}) + invariant(locus.to_position, 35, locus.to_coordinate, {'position': 4, 'offset': 1}) def test_Locus_inverted(): """Reverse orientent Lovus.""" locus = Locus((30, 35), True) - invariant(locus.to_position, 35, locus.to_coordinate, {"position": 0, "offset": -1}) - invariant(locus.to_position, 34, locus.to_coordinate, {"position": 0, "offset": 0}) - invariant(locus.to_position, 33, locus.to_coordinate, {"position": 1, "offset": 0}) - invariant(locus.to_position, 31, locus.to_coordinate, {"position": 3, "offset": 0}) - invariant(locus.to_position, 30, locus.to_coordinate, {"position": 4, "offset": 0}) - invariant(locus.to_position, 29, locus.to_coordinate, {"position": 4, "offset": 1}) + invariant(locus.to_position, 35, locus.to_coordinate, {'position': 0, 'offset': -1}) + invariant(locus.to_position, 34, locus.to_coordinate, {'position': 0, 'offset': 0}) + invariant(locus.to_position, 33, locus.to_coordinate, {'position': 1, 'offset': 0}) + invariant(locus.to_position, 31, locus.to_coordinate, {'position': 3, 'offset': 0}) + invariant(locus.to_position, 30, locus.to_coordinate, {'position': 4, 'offset': 0}) + invariant(locus.to_position, 29, locus.to_coordinate, {'position': 4, 'offset': 1}) def test_Locus_degenerate(): """Degenerate positions are silently corrected.""" locus = Locus((10, 20)) - degenerate_equal(locus.to_coordinate, 9, [{"position": 0, "offset": -1}, {"position": -1, "offset": 0}]) - degenerate_equal(locus.to_coordinate, 20, [{"position": 9, "offset": 1}, {"position": 10, "offset": 0}]) + degenerate_equal(locus.to_coordinate, 9, [{'position': 0, 'offset': -1}, {'position': -1, 'offset': 0}]) + degenerate_equal(locus.to_coordinate, 20, [{'position': 9, 'offset': 1}, {'position': 10, 'offset': 0}]) def test_Locus_inverted_degenerate(): """Degenerate positions are silently corrected.""" locus = Locus((10, 20), True) - degenerate_equal(locus.to_coordinate, 20, [{"position": 0, "offset": -1}, {"position": -1, "offset": 0}]) - degenerate_equal(locus.to_coordinate, 9, [{"position": 9, "offset": 1}, {"position": 10, "offset": 0}]) + degenerate_equal(locus.to_coordinate, 20, [{'position': 0, 'offset': -1}, {'position': -1, 'offset': 0}]) + degenerate_equal(locus.to_coordinate, 9, [{'position': 9, 'offset': 1}, {'position': 10, 'offset': 0}]) diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index 3434a62..ab3b877 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -35,14 +35,14 @@ def test_MultiLocus(): multi_locus.to_position, 4, multi_locus.to_coordinate, - {"position": 1, "offset": 0, "region": "u"}, + {'position': 1, 'offset': 0, 'region': 'u'}, ) invariant( multi_locus.to_position, 5, multi_locus.to_coordinate, - {"position": 0, "offset": 0, "region": ""}, + {'position': 0, 'offset': 0, 'region': ''}, ) # Internal locus. @@ -50,37 +50,37 @@ def test_MultiLocus(): multi_locus.to_position, 29, multi_locus.to_coordinate, - {"position": 9, "offset": -1, "region": ""}, + {'position': 9, 'offset': -1, 'region': ''}, ) invariant( multi_locus.to_position, 30, multi_locus.to_coordinate, - {"position": 9, "offset": 0, "region": ""}, + {'position': 9, 'offset': 0, 'region': ''}, ) invariant( multi_locus.to_position, 31, multi_locus.to_coordinate, - {"position": 10, "offset": 0, "region": ""}, + {'position': 10, 'offset': 0, 'region': ''}, ) invariant( multi_locus.to_position, 33, multi_locus.to_coordinate, - {"position": 12, "offset": 0, "region": ""}, + {'position': 12, 'offset': 0, 'region': ''}, ) invariant( multi_locus.to_position, 34, multi_locus.to_coordinate, - {"position": 13, "offset": 0, "region": ""}, + {'position': 13, 'offset': 0, 'region': ''}, ) invariant( multi_locus.to_position, 35, multi_locus.to_coordinate, - {"position": 13, "offset": 1, "region": ""}, + {'position': 13, 'offset': 1, 'region': ''}, ) # Boundary between the last locus and downstream. @@ -88,13 +88,13 @@ def test_MultiLocus(): multi_locus.to_position, 71, multi_locus.to_coordinate, - {"position": 21, "offset": 0, "region": ""}, + {'position': 21, 'offset': 0, 'region': ''}, ) invariant( multi_locus.to_position, 72, multi_locus.to_coordinate, - {"position": 1, "offset": 0, "region": "d"}, + {'position': 1, 'offset': 0, 'region': 'd'}, ) @@ -107,13 +107,13 @@ def test_MultiLocus_inverted(): multi_locus.to_position, 72, multi_locus.to_coordinate, - {"position": 1, "offset": 0, "region": "u"}, + {'position': 1, 'offset': 0, 'region': 'u'}, ) invariant( multi_locus.to_position, 71, multi_locus.to_coordinate, - {"position": 0, "offset": 0, "region": ""}, + {'position': 0, 'offset': 0, 'region': ''}, ) # Internal locus. @@ -121,37 +121,37 @@ def test_MultiLocus_inverted(): multi_locus.to_position, 35, multi_locus.to_coordinate, - {"position": 8, "offset": -1, "region": ""}, + {'position': 8, 'offset': -1, 'region': ''}, ) invariant( multi_locus.to_position, 34, multi_locus.to_coordinate, - {"position": 8, "offset": 0, "region": ""}, + {'position': 8, 'offset': 0, 'region': ''}, ) invariant( multi_locus.to_position, 33, multi_locus.to_coordinate, - {"position": 9, "offset": 0, "region": ""}, + {'position': 9, 'offset': 0, 'region': ''}, ) invariant( multi_locus.to_position, 31, multi_locus.to_coordinate, - {"position": 11, "offset": 0, "region": ""}, + {'position': 11, 'offset': 0, 'region': ''}, ) invariant( multi_locus.to_position, 30, multi_locus.to_coordinate, - {"position": 12, "offset": 0, "region": ""}, + {'position': 12, 'offset': 0, 'region': ''}, ) invariant( multi_locus.to_position, 29, multi_locus.to_coordinate, - {"position": 12, "offset": 1, "region": ""}, + {'position': 12, 'offset': 1, 'region': ''}, ) # Boundary between the last locus and downstream. @@ -159,13 +159,13 @@ def test_MultiLocus_inverted(): multi_locus.to_position, 5, multi_locus.to_coordinate, - {"position": 21, "offset": 0, "region": ""}, + {'position': 21, 'offset': 0, 'region': ''}, ) invariant( multi_locus.to_position, 4, multi_locus.to_coordinate, - {"position": 1, "offset": 0, "region": "d"}, + {'position': 1, 'offset': 0, 'region': 'd'}, ) @@ -177,13 +177,13 @@ def test_MultiLocus_adjacent_loci(): multi_locus.to_position, 2, multi_locus.to_coordinate, - {"position": 1, "offset": 0, "region": ""}, + {'position': 1, 'offset': 0, 'region': ''}, ) invariant( multi_locus.to_position, 3, multi_locus.to_coordinate, - {"position": 2, "offset": 0, "region": ""}, + {'position': 2, 'offset': 0, 'region': ''}, ) @@ -195,13 +195,13 @@ def test_MultiLocus_adjacent_loci_inverted(): multi_locus.to_position, 3, multi_locus.to_coordinate, - {"position": 1, "offset": 0, "region": ""}, + {'position': 1, 'offset': 0, 'region': ''}, ) invariant( multi_locus.to_position, 2, multi_locus.to_coordinate, - {"position": 2, "offset": 0, "region": ""}, + {'position': 2, 'offset': 0, 'region': ''}, ) @@ -213,13 +213,13 @@ def test_MultiLocus_offsets_odd(): multi_locus.to_position, 4, multi_locus.to_coordinate, - {"position": 1, "offset": 2, "region": ""}, + {'position': 1, 'offset': 2, 'region': ''}, ) invariant( multi_locus.to_position, 5, multi_locus.to_coordinate, - {"position": 2, "offset": -1, "region": ""}, + {'position': 2, 'offset': -1, 'region': ''}, ) @@ -231,13 +231,13 @@ def test_MultiLocus_offsets_odd_inverted(): multi_locus.to_position, 4, multi_locus.to_coordinate, - {"position": 1, "offset": 2, "region": ""}, + {'position': 1, 'offset': 2, 'region': ''}, ) invariant( multi_locus.to_position, 3, multi_locus.to_coordinate, - {"position": 2, "offset": -1, "region": ""}, + {'position': 2, 'offset': -1, 'region': ''}, ) @@ -249,13 +249,13 @@ def test_MultiLocus_offsets_even(): multi_locus.to_position, 4, multi_locus.to_coordinate, - {"position": 1, "offset": 2, "region": ""}, + {'position': 1, 'offset': 2, 'region': ''}, ) invariant( multi_locus.to_position, 5, multi_locus.to_coordinate, - {"position": 2, "offset": -2, "region": ""}, + {'position': 2, 'offset': -2, 'region': ''}, ) @@ -267,13 +267,13 @@ def test_MultiLocus_offsets_even_inverted(): multi_locus.to_position, 5, multi_locus.to_coordinate, - {"position": 1, "offset": 2, "region": ""}, + {'position': 1, 'offset': 2, 'region': ''}, ) invariant( multi_locus.to_position, 4, multi_locus.to_coordinate, - {"position": 2, "offset": -2, "region": ""}, + {'position': 2, 'offset': -2, 'region': ''}, ) @@ -285,9 +285,9 @@ def test_MultiLocus_degenerate(): multi_locus.to_coordinate, 4, [ - {"position": 0, "offset": -1, "region": "u"}, - {"position": 1, "offset": 0, "region": "u"}, - {"position": -1, "offset": 0, "region": "u"}, + {'position': 0, 'offset': -1, 'region': 'u'}, + {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': -1, 'offset': 0, 'region': 'u'}, ], ) @@ -295,8 +295,8 @@ def test_MultiLocus_degenerate(): multi_locus.to_coordinate, 72, [ - {"position": 0, "offset": 1, "region": "d"}, - {"position": 1, "offset": 0, "region": "d"}, + {'position': 0, 'offset': 1, 'region': 'd'}, + {'position': 1, 'offset': 0, 'region': 'd'}, ], ) @@ -309,9 +309,9 @@ def test_MultiLocus_inverted_degenerate(): multi_locus.to_coordinate, 72, [ - {"position": 0, "offset": 1, "region": "u"}, - {"position": -1, "offset": 0, "region": "u"}, - {"position": 1, "offset": 0, "region": "u"}, + {'position': 0, 'offset': 1, 'region': 'u'}, + {'position': -1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': 0, 'region': 'u'}, ], ) @@ -319,7 +319,7 @@ def test_MultiLocus_inverted_degenerate(): multi_locus.to_coordinate, 4, [ - {"position": 0, "offset": -1, "region": "d"}, - {"position": 1, "offset": 0, "region": "d"}, + {'position': 0, 'offset': -1, 'region': 'd'}, + {'position': 1, 'offset': 0, 'region': 'd'}, ], ) From 2f167e0d7abebbfe90a4e784f11b89af3202df08 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 20 Mar 2026 17:29:27 +0100 Subject: [PATCH 091/236] Cleanup, use typings --- mutalyzer_crossmapper/crossmapper.py | 28 ++++++++++++++-------------- mutalyzer_crossmapper/location.py | 4 ++-- mutalyzer_crossmapper/locus.py | 6 +++--- mutalyzer_crossmapper/multi_locus.py | 12 ++++++------ 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index e68bbbd..42ab6d5 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -3,7 +3,7 @@ class Genomic(object): """Genomic crossmap object.""" - def coordinate_to_genomic(self, coordinate): + def coordinate_to_genomic(self, coordinate: int) -> dict: """Convert a coordinate to a genomic position (g./m./o.). :arg int coordinate: Coordinate. @@ -12,7 +12,7 @@ def coordinate_to_genomic(self, coordinate): """ return {'position': coordinate + 1} - def genomic_to_coordinate(self, pos_m): + def genomic_to_coordinate(self, pos_m: dict) -> int: """Convert a genomic position (g./m./o.) to a coordinate. :arg dict pos_m: Genomic position model. @@ -24,7 +24,7 @@ def genomic_to_coordinate(self, pos_m): class NonCoding(Genomic): """NonCoding crossmap object.""" - def __init__(self, locations, inverted=False): + def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> None: """ :arg list locations: List of locus locations. :arg bool inverted: Orientation. @@ -33,7 +33,7 @@ def __init__(self, locations, inverted=False): self._noncoding = MultiLocus(locations, inverted) - def coordinate_to_noncoding(self, coordinate): + def coordinate_to_noncoding(self, coordinate: int) -> dict: """Convert a coordinate to a noncoding position (n./r.). :arg int coordinate: Coordinate. @@ -45,7 +45,7 @@ def coordinate_to_noncoding(self, coordinate): pos_m['position'] = pos_m['position'] + 1 return pos_m - def noncoding_to_coordinate(self, pos_m): + def noncoding_to_coordinate(self, pos_m: dict) -> int: """Convert a noncoding position (n./r.) to a coordinate. :arg dict pos_m: Noncoding position model. @@ -60,7 +60,7 @@ def noncoding_to_coordinate(self, pos_m): class Coding(NonCoding): """Coding crossmap object.""" - def __init__(self, locations, cds, inverted=False): + def __init__(self, locations: list[tuple[int,int]], cds: tuple[int,int], inverted : bool=False) -> None: """ :arg list locations: List of locus locations. :arg tuple cds: Locus location. @@ -80,7 +80,7 @@ def __init__(self, locations, cds, inverted=False): self._coding = (b0['position'] + b0['offset'], b1['position'] + b1['offset'] +1) self._exons = (e0['position'], e1['position']) - def _degenerate_position(self, pos_m): + def _degenerate_position(self, pos_m: dict) -> dict: """Degenerate a coding position model (c./r.). :arg dict pos_m: Coding position model. @@ -106,7 +106,7 @@ def _degenerate_position(self, pos_m): degenerated_pos_m['region'] = '*' return degenerated_pos_m - def _normalize_position(self, pos_m): + def _normalize_position(self, pos_m: dict) -> dict: """Normalize a coding position model (c./r.). :arg dict pos_m: Coding position model. @@ -121,7 +121,7 @@ def _normalize_position(self, pos_m): coordinate = coordinate + pos_m['offset'] return self.coordinate_to_coding(coordinate) - def _coordinate_to_coding(self, coordinate): + def _coordinate_to_coding(self, coordinate: int) -> dict: """Convert a coordinate to a coding position (c./r.). :arg int coordinate: Coordinate. @@ -153,7 +153,7 @@ def _coordinate_to_coding(self, coordinate): 'region': '' } - def coordinate_to_coding(self, coordinate, degenerate=False): + def coordinate_to_coding(self, coordinate: tuple[int, int], degenerate: bool=False) -> dict: """Convert a coordinate to a coding position (c./r.). :arg int coordinate: Coordinate. @@ -168,7 +168,7 @@ def coordinate_to_coding(self, coordinate, degenerate=False): return pos_m - def _coding_to_coordinate(self, pos_m): + def _coding_to_coordinate(self, pos_m: dict) -> int: """Convert a coding position (c./r.) to a coordinate. :arg dict pos_m: Coding position model (c./r.). @@ -191,7 +191,7 @@ def _coding_to_coordinate(self, pos_m): return self._noncoding.to_coordinate(noncoding_pos_m) - def coding_to_coordinate(self, pos_m): + def coding_to_coordinate(self, pos_m: dict) -> int: """Convert a coding position (c./r.) to a coordinate. :arg dict pos_m: Coding position model (c./r.). @@ -202,7 +202,7 @@ def coding_to_coordinate(self, pos_m): return self._coding_to_coordinate(normalized_pos_m) - def coordinate_to_protein(self, coordinate): + def coordinate_to_protein(self, coordinate: int) -> dict: """Convert a coordinate to a protein position (p.). :arg int coordinate: Coordinate. @@ -227,7 +227,7 @@ def coordinate_to_protein(self, coordinate): 'position_in_codon': (position + 2) % 3 + 1, **{k: v for k, v in pos.items() if k != 'position'}} - def protein_to_coordinate(self, pos_m): + def protein_to_coordinate(self, pos_m: dict) -> int: """Convert a protein position (p.) to a coordinate. :arg dict position: Protein position model(p.). diff --git a/mutalyzer_crossmapper/location.py b/mutalyzer_crossmapper/location.py index e580672..1bf7a06 100644 --- a/mutalyzer_crossmapper/location.py +++ b/mutalyzer_crossmapper/location.py @@ -1,4 +1,4 @@ -def _nearest_boundary(lb, rb, c, p): +def _nearest_boundary(lb: int, rb: int, c: int, p: int) -> int: """Find the boundary nearest to `c`. In case of a draw, the parameter `p` decides which one is chosen. @@ -19,7 +19,7 @@ def _nearest_boundary(lb, rb, c, p): return p -def nearest_location(ls, c, p=0): +def nearest_location(ls: list[tuple[int,int]], c: int, p: int = 0) -> int: """Find the location nearest to `c`. In case of a draw, the parameter `p` decides which index is chosen. diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index eb23efd..15b15a0 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -1,6 +1,6 @@ class Locus(object): """Locus object.""" - def __init__(self, location, inverted=False): + def __init__(self, location: list[tuple[int, int]], inverted=False) -> None: """ :arg tuple location: Locus location. :arg bool inverted: Orientation. @@ -10,7 +10,7 @@ def __init__(self, location, inverted=False): self.boundary = location[0], location[1] - 1 self._end = self.boundary[1] - self.boundary[0] - def to_position(self, coordinate): + def to_position(self, coordinate: int) -> dict: """Convert a coordinate to a proper position model. :arg int coordinate: Coordinate. @@ -30,7 +30,7 @@ def to_position(self, coordinate): return {'position': self._end, 'offset': coordinate - self.boundary[1]} return {'position': coordinate - self.boundary[0], 'offset': 0} - def to_coordinate(self, pos_m): + def to_coordinate(self, pos_m: dict) -> int: """Convert a position model to a coordinate. :arg dict position: Position model with 'position' and 'offset' keys. diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index c76a39b..001ed2c 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -5,7 +5,7 @@ from .locus import Locus -def _offsets(locations, orientation): +def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: """For each location, calculate the length of the preceding locations. :arg list locations: List of locations. @@ -19,7 +19,7 @@ def _offsets(locations, orientation): class MultiLocus(object): """MultiLocus object.""" - def __init__(self, locations:list, inverted=False): + def __init__(self, locations: list[tuple[int, int]], inverted=False) -> None: """ :arg list locations: List of locus locations. :arg bool inverted: Orientation. @@ -31,12 +31,12 @@ def __init__(self, locations:list, inverted=False): self._orientation = -1 if inverted else 1 self._offsets = _offsets(locations, self._orientation) - def _direction(self, index): + def _direction(self, index: int) -> int: if self._inverted: return len(self._offsets) - index - 1 return index - def outside(self, coordinate:int): + def outside(self, coordinate: int) -> int: """Calculate the offset relative to this MultiLocus. :arg int coordinate: Coordinate. @@ -49,7 +49,7 @@ def outside(self, coordinate:int): return coordinate - self._loci[-1].boundary[1] return 0 - def to_position(self, coordinate:int): + def to_position(self, coordinate: int) -> dict: """Convert a coordinate to a position. :arg int coordinate: Coordinate. @@ -74,7 +74,7 @@ def to_position(self, coordinate:int): 'region': region } - def to_coordinate(self, pos_m:dict): + def to_coordinate(self, pos_m: dict) -> int: """Convert a position model to a coordinate. :arg dict pos_m: Position model with 'position','offset' and 'region' keys. From ac0cd16d0cbfdfb49abcab494f5d52228fe36a7d Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 23 Mar 2026 10:35:49 +0100 Subject: [PATCH 092/236] Cleanup --- docs/library.rst | 46 ++++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index a1c236e..83bf73a 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -22,7 +22,7 @@ They are represented as 1-key dictionaries. Below is an example of ``g.1`` in HG Where: -- **position**: a positive integer repersenting a base position(>0) +- **position**: an integer repersenting a base position (>0) Genomic Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -32,7 +32,7 @@ Genomic Position Conversion >>> from mutalyzer_crossmapper import Genomic >>> crossmap = Genomic() -The functions ``coordinate_to_genomic()`` and ``genomic_to_coordinate`` can be +The functions ``coordinate_to_genomic()`` and ``genomic_to_coordinate()`` can be used to convert to and from genomic positions. .. code:: python @@ -181,7 +181,7 @@ The ``Coding`` class -------------------- The ``Coding`` class provides an interface to all conversions between -coding (``c.``, ``r.``) rpositioning systems and coordinates. Conversions between +coding (``c.``, ``r.``) positions and coordinates. Conversions between positioning systems should be done via a coordinate. Coding Position Model @@ -307,6 +307,11 @@ In the following table, we show a number of annotated examples. - 0 - ``*`` - ``c.*5`` + * - 72 + - 1 + - 0 + - ``d`` + - ``c.d1`` * - 79 - 8 - 0 @@ -348,8 +353,8 @@ position ``p.2``. We can convert between these to as follows. >>> crossmap.protein_to_coordinate({'position':2, 'position_in_codon':2, 'offset':0, 'region':''}) 41 -Note that the protein position only corresponds with the HGVS "p." notation -when the offset equals ``0`` and the region equals ``1``. In the following +**Note:** protein position only corresponds with the HGVS "p." notation +when the offset equals ``0`` and the region equals ``''``. In the following table, we show a number of annotated examples. .. _table_protein: @@ -380,18 +385,6 @@ table, we show a number of annotated examples. - 0 - ``-`` - invalid - * - 7 - - 3 - - 1 - - 0 - - ``-`` - - invalid - * - 8 - - 3 - - 1 - - 1 - - ``-`` - - invalid * - 31 - 1 - 3 @@ -410,6 +403,18 @@ table, we show a number of annotated examples. - 0 - - ``p.1`` + * - 34 + - 1 + - 3 + - 0 + - + - ``p.1`` + * - 35 + - 1 + - 3 + - 1 + - + - ``p.1`` * - 42 - 2 - 3 @@ -476,9 +481,6 @@ The ``Locus`` class The ``Locus`` class is used to deal with offsets with respect to a single locus. -**Note:** the ``position`` values in the position dictionaries are **0-based**, -so the first base of the locus corresponds to ``{'position': 0, 'offset': 0}``. -This differs from HGVS numbering, which is **1-based**. .. code:: python @@ -490,6 +492,10 @@ converting from a locus position to a coordinate and vice versa. These functions work with a 2-key dictionary, see the section about `The NonCoding class`_ for the semantics. +**Note:** the ``position`` values in the position dictionaries are **0-based**, +so the first base of the locus corresponds to ``{'position': 0, 'offset': 0}``. +This differs from HGVS numbering, which is **1-based**. + .. code:: python >>> locus.to_position(9) From 2ebf6c95df03a381b3f2cba8b5ebc82fd5266ef0 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 23 Mar 2026 11:08:40 +0100 Subject: [PATCH 093/236] Cleanup --- Makefile | 20 -------------------- conf.py | 28 ---------------------------- index.rst | 17 ----------------- make.bat | 35 ----------------------------------- 4 files changed, 100 deletions(-) delete mode 100644 Makefile delete mode 100644 conf.py delete mode 100644 index.rst delete mode 100644 make.bat diff --git a/Makefile b/Makefile deleted file mode 100644 index d4bb2cb..0000000 --- a/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/conf.py b/conf.py deleted file mode 100644 index a50b31a..0000000 --- a/conf.py +++ /dev/null @@ -1,28 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information - -project = 'crossmapper_dict' -copyright = '2026, Xiaoyun Liu' -author = 'Xiaoyun Liu' -release = '1.0.0' - -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - -extensions = [] - -templates_path = ['_templates'] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - - - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - -html_theme = 'alabaster' -html_static_path = ['_static'] diff --git a/index.rst b/index.rst deleted file mode 100644 index 3d88e72..0000000 --- a/index.rst +++ /dev/null @@ -1,17 +0,0 @@ -.. crossmapper_dict documentation master file, created by - sphinx-quickstart on Fri Mar 20 15:13:36 2026. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -crossmapper_dict documentation -============================== - -Add your content using ``reStructuredText`` syntax. See the -`reStructuredText `_ -documentation for details. - - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - diff --git a/make.bat b/make.bat deleted file mode 100644 index 32bb245..0000000 --- a/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd From f2e8efa7e49b0706111dc97970d4e190a189f62a Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 23 Mar 2026 11:13:44 +0100 Subject: [PATCH 094/236] Stop trackinhg local file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 381420f..324acae 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ docs/_build/ mutalyzer_crossmapper.egg-info/ mutalyzer_crossmapper/__pycache__/ tests/__pycache__/ +tmp \ No newline at end of file From 4e6443c797c35760367dafb93fcaf3da48c2781e Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 23 Mar 2026 11:16:27 +0100 Subject: [PATCH 095/236] Delete tmp files from branch --- tmp/t.t | 80 ------------------------------------------ tmp/test_degenerate.py | 51 --------------------------- 2 files changed, 131 deletions(-) delete mode 100644 tmp/t.t delete mode 100644 tmp/test_degenerate.py diff --git a/tmp/t.t b/tmp/t.t deleted file mode 100644 index b473489..0000000 --- a/tmp/t.t +++ /dev/null @@ -1,80 +0,0 @@ -"0", "4", "2", "0", "u" -"1", "4", "2", "0", "u" -"2", "4", "2", "0", "u" -"3", "4", "2", "0", "u" -"4", "4", "2", "0", "u" -"5", "4", "2", "0", "-" -"6", "4", "3", "0", "-" -"7", "3", "1", "0", "-" -"8", "3", "1", "1", "-" -"9", "3", "1", "2", "-" -"10", "3", "1", "3", "-" -"11", "3", "2", "-3", "-" -"12", "3", "2", "-2", "-" -"13", "3", "2", "-1", "-" -"14", "3", "2", "0", "-" -"15", "3", "3", "0", "-" -"16", "2", "1", "0", "-" -"17", "2", "2", "0", "-" -"18", "2", "3", "0", "-" -"19", "1", "1", "0", "-" -"20", "1", "1", "1", "-" -"21", "1", "1", "2", "-" -"22", "1", "1", "3", "-" -"23", "1", "1", "4", "-" -"24", "1", "1", "5", "-" -"25", "1", "2", "-5", "-" -"26", "1", "2", "-4", "-" -"27", "1", "2", "-3", "-" -"28", "1", "2", "-2", "-" -"29", "1", "2", "-1", "-" -"30", "1", "2", "0", "-" -"31", "1", "3", "0", "-" -"32", "1", "1", "0", "" -"33", "1", "2", "0", "" -"34", "1", "3", "0", "" -"35", "1", "3", "1", "" -"36", "1", "3", "2", "" -"37", "1", "3", "3", "" -"38", "2", "1", "-2", "" -"39", "2", "1", "-1", "" -"40", "2", "1", "0", "" -"41", "2", "2", "0", "" -"42", "2", "3", "0", "" -"43", "1", "1", "0", "*" -"44", "1", "1", "1", "*" -"45", "1", "1", "2", "*" -"46", "1", "1", "3", "*" -"47", "1", "2", "-3", "*" -"48", "1", "2", "-2", "*" -"49", "1", "2", "-1", "*" -"50", "1", "2", "0", "*" -"51", "1", "3", "0", "*" -"52", "1", "3", "1", "*" -"53", "1", "3", "2", "*" -"54", "1", "3", "3", "*" -"55", "1", "3", "4", "*" -"56", "1", "3", "5", "*" -"57", "1", "3", "6", "*" -"58", "1", "3", "7", "*" -"59", "1", "3", "8", "*" -"60", "1", "3", "9", "*" -"61", "2", "1", "-9", "*" -"62", "2", "1", "-8", "*" -"63", "2", "1", "-7", "*" -"64", "2", "1", "-6", "*" -"65", "2", "1", "-5", "*" -"66", "2", "1", "-4", "*" -"67", "2", "1", "-3", "*" -"68", "2", "1", "-2", "*" -"69", "2", "1", "-1", "*" -"70", "2", "1", "0", "*" -"71", "2", "2", "0", "*" -"72", "2", "2", "0", "d" -"73", "2", "2", "0", "d" -"74", "2", "2", "0", "d" -"75", "2", "2", "0", "d" -"76", "2", "2", "0", "d" -"77", "2", "2", "0", "d" -"78", "2", "2", "0", "d" -"79", "2", "2", "0", "d" diff --git a/tmp/test_degenerate.py b/tmp/test_degenerate.py deleted file mode 100644 index c513d0e..0000000 --- a/tmp/test_degenerate.py +++ /dev/null @@ -1,51 +0,0 @@ -"""a script to check degenerate option""" -from mutalyzer_crossmapper import Coding, Genomic, NonCoding - - -def serialize(pos_m: dict): - if pos_m["offset"] > 0: - return f"{pos_m['region']}{pos_m['position']}+{pos_m['offset']}" - elif pos_m["offset"] < 0: - return f"{pos_m['region']}{pos_m['position']}{pos_m['offset']}" - else: - return f"{pos_m['region']}{pos_m['position']}" - -_exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] -_cds = (32, 43) - - -nc_crossmap = NonCoding(_exons, True) -c_crossmap = Coding(_exons, _cds) -test = Coding([(10, 11)], (10, 11)) - - -for i in range(0, 80): - # nc = nc_crossmap.coordinate_to_noncoding(i) - c = c_crossmap.coordinate_to_protein(i) - - # print(i, c, c_crossmap.coding_to_coordinate(c)) - # c_de = nc_crossmap.coordinate_to_coding(i, True) - # nc_de = nc_crossmap.coordinate_to_coding(i, True) - print(i, c) - # print(f'"{i}", "{c["position"]}", "{c["position_in_codon"]}", "{c["offset"]}", "{c["region"]}"') - - -# crossmap = Coding(_exons, _cds) -# for i in range(0, 80): -# print(i, crossmap.coordinate_to_coding(i), crossmap.coordinate_to_coding(i, degenerate=True)) - -# nc_crossmap = NonCoding(_exons) -# for i in range(0, 80): -# print(i, nc_crossmap.coordinate_to_noncoding(i)) - - - - - - -# degereate option -""" -With this option, it keeps counting c_pos outside the exons range -e.g., (-16, 0, -1, -5) means -c_pos=16, offset_to_c_pos=0, before_CDS, offset_to_exons_range = -5 -""" From 8deedc15cfc7d1aab64a9d47833cdf54d62ff2ff Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 23 Mar 2026 11:43:08 +0100 Subject: [PATCH 096/236] Cleanup --- mutalyzer_crossmapper/crossmapper.py | 19 ++++++++++--------- mutalyzer_crossmapper/locus.py | 2 +- mutalyzer_crossmapper/multi_locus.py | 5 ++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 42ab6d5..6d4f656 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -88,10 +88,11 @@ def _degenerate_position(self, pos_m: dict) -> dict: :returns dict: a generate coding position model. """ region = pos_m['region'] - position = pos_m['position'] + if region not in ('u', 'd'): + return pos_m degenerated_pos_m = {'offset': pos_m['offset']} - + position = pos_m['position'] if region == 'u': if self._inverted: degenerated_pos_m['position'] = position + self._exons[1] - self._coding[1] + 1 @@ -113,13 +114,13 @@ def _normalize_position(self, pos_m: dict) -> dict: :returns dict: a normalized coding postion model. """ - initial_pos = {**pos_m, 'offset': 0} - coordinate = self._coding_to_coordinate(initial_pos) + base_pos = {**pos_m, 'offset': 0} + base_coordinate = self._coding_to_coordinate(base_pos) if self._inverted: - coordinate = coordinate - pos_m['offset'] + base_coordinate = base_coordinate - pos_m['offset'] else: - coordinate = coordinate + pos_m['offset'] - return self.coordinate_to_coding(coordinate) + base_coordinate = base_coordinate + pos_m['offset'] + return self.coordinate_to_coding(base_coordinate) def _coordinate_to_coding(self, coordinate: int) -> dict: """Convert a coordinate to a coding position (c./r.). @@ -163,7 +164,7 @@ def coordinate_to_coding(self, coordinate: tuple[int, int], degenerate: bool=Fal """ pos_m = self._coordinate_to_coding(coordinate) - if degenerate and pos_m['region'] in ('u', 'd'): + if degenerate: pos_m = self._degenerate_position(pos_m) return pos_m @@ -209,7 +210,7 @@ def coordinate_to_protein(self, coordinate: int) -> dict: :returns dict: Protein position model(p.). """ - pos = self.coordinate_to_coding(coordinate) + pos = self.coordinate_to_coding(coordinate, True) if pos['region'] == 'u': pos = self.coordinate_to_coding(coordinate + pos['position']) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 15b15a0..fff0c66 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -10,7 +10,7 @@ def __init__(self, location: list[tuple[int, int]], inverted=False) -> None: self.boundary = location[0], location[1] - 1 self._end = self.boundary[1] - self.boundary[0] - def to_position(self, coordinate: int) -> dict: + def to_position(self, coordinate: int) -> dict[str, int]: """Convert a coordinate to a proper position model. :arg int coordinate: Coordinate. diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 001ed2c..0b1ef52 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -19,7 +19,7 @@ def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: class MultiLocus(object): """MultiLocus object.""" - def __init__(self, locations: list[tuple[int, int]], inverted=False) -> None: + def __init__(self, locations: list[tuple[int, int]], inverted: bool=False) -> None: """ :arg list locations: List of locus locations. :arg bool inverted: Orientation. @@ -49,7 +49,7 @@ def outside(self, coordinate: int) -> int: return coordinate - self._loci[-1].boundary[1] return 0 - def to_position(self, coordinate: int) -> dict: + def to_position(self, coordinate: int) -> dict[str: int | str]: """Convert a coordinate to a position. :arg int coordinate: Coordinate. @@ -67,7 +67,6 @@ def to_position(self, coordinate: int) -> dict: 'offset': 0, 'region': region } - return { 'position': location['position'] + self._offsets[self._direction(index)], 'offset': location['offset'], From f5f7a51353337a4795b78b96c6aea9fba3a0cb3b Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 23 Mar 2026 16:19:50 +0100 Subject: [PATCH 097/236] Refactor protein conversion --- docs/library.rst | 13 ++++++++++--- mutalyzer_crossmapper/crossmapper.py | 13 ++++--------- tests/test_crossmapper.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 83bf73a..118c9e8 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -368,14 +368,14 @@ table, we show a number of annotated examples. - region - HGVS * - 0 - - 4 + - 2 - 2 - 0 - ``u`` - invalid * - 4 - - 4 - - 2 + - 1 + - 3 - 0 - ``u`` - invalid @@ -433,6 +433,13 @@ table, we show a number of annotated examples. - 1 - ``*`` - invalid + * - 72 + - 1 + - 1 + - 0 + - ``d`` + - invalid + * - 79 - 2 - 2 diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 6d4f656..4ac0ce4 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -77,7 +77,7 @@ def __init__(self, locations: list[tuple[int,int]], cds: tuple[int,int], inverte self._coding = (b1['position'] + b1['offset'], b0['position'] + b0['offset'] + 1) self._exons = (e1['position'], e0['position']) else: - self._coding = (b0['position'] + b0['offset'], b1['position'] + b1['offset'] +1) + self._coding = (b0['position'] + b0['offset'], b1['position'] + b1['offset'] + 1) self._exons = (e0['position'], e1['position']) def _degenerate_position(self, pos_m: dict) -> dict: @@ -210,15 +210,10 @@ def coordinate_to_protein(self, coordinate: int) -> dict: :returns dict: Protein position model(p.). """ - pos = self.coordinate_to_coding(coordinate, True) - - if pos['region'] == 'u': - pos = self.coordinate_to_coding(coordinate + pos['position']) - elif pos['region'] == 'd': - pos = self.coordinate_to_coding(coordinate - pos['position']) + pos = self.coordinate_to_coding(coordinate) position = pos['position'] - if pos['region'] == '-': + if pos['region'] in ('-', 'u'): return { 'position': abs(-position // 3), 'position_in_codon': -position % 3 + 1, @@ -235,7 +230,7 @@ def protein_to_coordinate(self, pos_m: dict) -> int: :returns int: Coordinate. """ - if pos_m['region'] == '-': + if pos_m['region'] in ('-', 'u'): return self.coding_to_coordinate( {'position': 3 * pos_m['position'] - pos_m['position_in_codon'] + 1, 'offset': pos_m['offset'], diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 97c6262..7265507 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -667,6 +667,20 @@ def test_Coding_protein(): """Protein positions.""" crossmap = Coding(_exons, _cds) + # Boundary between upstream and 5' UTR + invariant( + crossmap.coordinate_to_protein, + 4, + crossmap.protein_to_coordinate, + {'position': 1, 'position_in_codon': 3, 'offset': 0, 'region': 'u'} + ) + invariant( + crossmap.coordinate_to_protein, + 5, + crossmap.protein_to_coordinate, + {'position': 4, 'position_in_codon': 2, 'offset': 0, 'region': '-'} + ) + # Boundary between 5' UTR and CDS invariant( crossmap.coordinate_to_protein, @@ -708,3 +722,17 @@ def test_Coding_protein(): crossmap.protein_to_coordinate, {'position': 1, 'position_in_codon': 1, 'offset': 0, 'region': '*'}, ) + + # Boundary between 3' UTR and downstream + invariant( + crossmap.coordinate_to_protein, + 71, + crossmap.protein_to_coordinate, + {'position': 2, 'position_in_codon': 2, 'offset': 0, 'region': '*'} + ) + invariant( + crossmap.coordinate_to_protein, + 72, + crossmap.protein_to_coordinate, + {'position': 1, 'position_in_codon': 1, 'offset': 0, 'region': 'd'} + ) From 79cbcc0266ea94626c00b08b6f88cdde9e44c99d Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 23 Mar 2026 16:39:00 +0100 Subject: [PATCH 098/236] Add degenerate tests --- tests/test_multi_locus.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index ab3b877..259eafa 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -285,7 +285,8 @@ def test_MultiLocus_degenerate(): multi_locus.to_coordinate, 4, [ - {'position': 0, 'offset': -1, 'region': 'u'}, + {'position': 0, 'offset': -1, 'region': ''}, + {'position': -1, 'offset': 0, 'region': ''}, {'position': 1, 'offset': 0, 'region': 'u'}, {'position': -1, 'offset': 0, 'region': 'u'}, ], @@ -295,6 +296,8 @@ def test_MultiLocus_degenerate(): multi_locus.to_coordinate, 72, [ + {'position': 21, 'offset': 1, 'region': ''}, + {'position': 22, 'offset': 0, 'region': ''}, {'position': 0, 'offset': 1, 'region': 'd'}, {'position': 1, 'offset': 0, 'region': 'd'}, ], @@ -310,7 +313,8 @@ def test_MultiLocus_inverted_degenerate(): 72, [ {'position': 0, 'offset': 1, 'region': 'u'}, - {'position': -1, 'offset': 0, 'region': 'u'}, + {'position': -1, 'offset': 0, 'region': ''}, + {'position': 0, 'offset': -1, 'region': ''}, {'position': 1, 'offset': 0, 'region': 'u'}, ], ) @@ -320,6 +324,8 @@ def test_MultiLocus_inverted_degenerate(): 4, [ {'position': 0, 'offset': -1, 'region': 'd'}, + {'position': 21, 'offset': 1, 'region': ''}, + {'position': 22, 'offset': 0, 'region': ''}, {'position': 1, 'offset': 0, 'region': 'd'}, ], ) From 1459451f4f263b80713b01cbc56c5c6731154724 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 23 Mar 2026 16:58:03 +0100 Subject: [PATCH 099/236] Add backticks for region in table --- docs/library.rst | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 118c9e8..a069719 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -137,12 +137,12 @@ In the following table, we show a number of annotated examples. * - 0 - 5 - 0 - - ``u`` + - ``'u'`` - ``n.u5`` * - 4 - 1 - 0 - - ``u`` + - ``'u'`` - ``n.u1`` * - 5 - 1 @@ -167,12 +167,12 @@ In the following table, we show a number of annotated examples. * - 72 - 1 - 0 - - ``d`` + - ``'d'`` - ``n.d1`` * - 79 - 8 - 0 - - ``d`` + - ``'d'`` - ``n.d8`` See section :doc:`api/crossmap` for a detailed description. @@ -255,27 +255,27 @@ In the following table, we show a number of annotated examples. * - 0 - 5 - 0 - - ``u`` + - ``'u'`` - ``c.u5`` * - 4 - 1 - 0 - - ``u`` + - ``'u'`` - ``c.u1`` * - 5 - 11 - 0 - - ``-`` + - ``'-'`` - ``c.-11`` * - 24 - 3 - 5 - - ``-`` + - ``'-'`` - ``c.-3+5`` * - 31 - 1 - 0 - - ``-`` + - ``'-'`` - ``c.-1`` * - 32 - 1 @@ -295,27 +295,27 @@ In the following table, we show a number of annotated examples. * - 43 - 1 - 0 - - ``*`` + - ``'*'`` - ``c.*1`` * - 61 - 4 - -9 - - ``*`` + - ``'*'`` - ``c.*4-9`` * - 71 - 5 - 0 - - ``*`` + - ``'*'`` - ``c.*5`` * - 72 - 1 - 0 - - ``d`` + - ``'d'`` - ``c.d1`` * - 79 - 8 - 0 - - ``d`` + - ``'d'`` - ``c.d8`` @@ -371,25 +371,25 @@ table, we show a number of annotated examples. - 2 - 2 - 0 - - ``u`` + - ``'u'`` - invalid * - 4 - 1 - 3 - 0 - - ``u`` + - ``'u'`` - invalid * - 5 - 4 - 2 - 0 - - ``-`` + - ``'-'`` - invalid * - 31 - 1 - 3 - 0 - - ``-`` + - ``'-'`` - invalid * - 32 - 1 @@ -425,26 +425,26 @@ table, we show a number of annotated examples. - 1 - 1 - 0 - - ``*`` + - ``'*'`` - invalid * - 44 - 1 - 1 - 1 - - ``*`` + - ``'*'`` - invalid * - 72 - 1 - 1 - 0 - - ``d`` + - ``'d'`` - invalid * - 79 - 2 - 2 - 0 - - ``d`` + - ``'d'`` - invalid From e92c78e5276187cc798796720c56c8d09ef4c385 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 23 Mar 2026 17:38:39 +0100 Subject: [PATCH 100/236] Cleanup --- mutalyzer_crossmapper/crossmapper.py | 38 ++++++++++++++-------------- mutalyzer_crossmapper/multi_locus.py | 8 +++--- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 4ac0ce4..4630e7d 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -53,7 +53,7 @@ def noncoding_to_coordinate(self, pos_m: dict) -> int: :returns int: Coordinate. """ multilocus_pos_m = {**pos_m} - if pos_m['region'] == '': + if multilocus_pos_m['region'] == '': multilocus_pos_m['position'] = pos_m['position'] - 1 return self._noncoding.to_coordinate(multilocus_pos_m) @@ -91,21 +91,21 @@ def _degenerate_position(self, pos_m: dict) -> dict: if region not in ('u', 'd'): return pos_m - degenerated_pos_m = {'offset': pos_m['offset']} - position = pos_m['position'] + degenerate_pos_m = {'offset': pos_m['offset']} + location = pos_m['position'] if region == 'u': if self._inverted: - degenerated_pos_m['position'] = position + self._exons[1] - self._coding[1] + 1 + degenerate_pos_m['position'] = location + self._exons[1] - self._coding[1] + 1 else: - degenerated_pos_m['position'] = position + self._coding[0] - degenerated_pos_m['region'] = '-' + degenerate_pos_m['position'] = location + self._coding[0] + degenerate_pos_m['region'] = '-' if region == 'd': if self._inverted: - degenerated_pos_m['position'] = position + self._coding[0] + degenerate_pos_m['position'] = location + self._coding[0] else: - degenerated_pos_m['position'] = position + self._exons[1]- self._coding[1] + 1 - degenerated_pos_m['region'] = '*' - return degenerated_pos_m + degenerate_pos_m['position'] = location + self._exons[1]- self._coding[1] + 1 + degenerate_pos_m['region'] = '*' + return degenerate_pos_m def _normalize_position(self, pos_m: dict) -> dict: """Normalize a coding position model (c./r.). @@ -176,7 +176,7 @@ def _coding_to_coordinate(self, pos_m: dict) -> int: :returns int: Coordinate. """ - position = pos_m['position'] + location = pos_m['position'] region = pos_m['region'] if region in ('u', 'd'): @@ -184,11 +184,11 @@ def _coding_to_coordinate(self, pos_m: dict) -> int: noncoding_pos_m = {'offset': pos_m['offset'], 'region': ''} if region == '': - noncoding_pos_m['position'] = position + self._coding[0] - 1 + noncoding_pos_m['position'] = location + self._coding[0] - 1 elif region == '-': - noncoding_pos_m['position'] = self._coding[0] - position + noncoding_pos_m['position'] = self._coding[0] - location else: - noncoding_pos_m['position'] = self._coding[1] + position - 1 + noncoding_pos_m['position'] = self._coding[1] + location - 1 return self._noncoding.to_coordinate(noncoding_pos_m) @@ -212,15 +212,15 @@ def coordinate_to_protein(self, coordinate: int) -> dict: """ pos = self.coordinate_to_coding(coordinate) - position = pos['position'] + location = pos['position'] if pos['region'] in ('-', 'u'): return { - 'position': abs(-position // 3), - 'position_in_codon': -position % 3 + 1, + 'position': abs(-location // 3), + 'position_in_codon': -location % 3 + 1, **{k: v for k, v in pos.items() if k != 'position'}} return { - 'position': (position + 2) // 3, - 'position_in_codon': (position + 2) % 3 + 1, + 'position': (location + 2) // 3, + 'position_in_codon': (location + 2) % 3 + 1, **{k: v for k, v in pos.items() if k != 'position'}} def protein_to_coordinate(self, pos_m: dict) -> int: diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 0b1ef52..2f67777 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -59,17 +59,17 @@ def to_position(self, coordinate: int) -> dict[str: int | str]: index = nearest_location(self._locations, coordinate, self._inverted) outside = self._orientation * self.outside(coordinate) region = 'u' if outside < 0 else 'd' if outside > 0 else '' - location = self._loci[index].to_position(coordinate) + locus_pos_m = self._loci[index].to_position(coordinate) if outside: return { - 'position': abs(location['offset']), + 'position': abs(locus_pos_m['offset']), 'offset': 0, 'region': region } return { - 'position': location['position'] + self._offsets[self._direction(index)], - 'offset': location['offset'], + 'position': locus_pos_m['position'] + self._offsets[self._direction(index)], + 'offset': locus_pos_m['offset'], 'region': region } From f8105676ff7f1f01c296f1887d778644c9d184ce Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 25 Mar 2026 13:54:51 +0100 Subject: [PATCH 101/236] Discard normalize position model --- mutalyzer_crossmapper/crossmapper.py | 25 +++++++------------------ mutalyzer_crossmapper/multi_locus.py | 13 +++++++------ 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 4630e7d..6ad0140 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -45,7 +45,7 @@ def coordinate_to_noncoding(self, coordinate: int) -> dict: pos_m['position'] = pos_m['position'] + 1 return pos_m - def noncoding_to_coordinate(self, pos_m: dict) -> int: + def noncoding_to_coordinate(self, pos_m: dict, degenerate: bool=True) -> int: """Convert a noncoding position (n./r.) to a coordinate. :arg dict pos_m: Noncoding position model. @@ -53,6 +53,11 @@ def noncoding_to_coordinate(self, pos_m: dict) -> int: :returns int: Coordinate. """ multilocus_pos_m = {**pos_m} + if degenerate: + if multilocus_pos_m["region"] == '-': + multilocus_pos_m["region"] = 'u' + elif multilocus_pos_m["region"] == '*': + multilocus_pos_m['region'] = 'd' if multilocus_pos_m['region'] == '': multilocus_pos_m['position'] = pos_m['position'] - 1 return self._noncoding.to_coordinate(multilocus_pos_m) @@ -107,21 +112,6 @@ def _degenerate_position(self, pos_m: dict) -> dict: degenerate_pos_m['region'] = '*' return degenerate_pos_m - def _normalize_position(self, pos_m: dict) -> dict: - """Normalize a coding position model (c./r.). - - :arg dict pos_m: Coding position model. - - :returns dict: a normalized coding postion model. - """ - base_pos = {**pos_m, 'offset': 0} - base_coordinate = self._coding_to_coordinate(base_pos) - if self._inverted: - base_coordinate = base_coordinate - pos_m['offset'] - else: - base_coordinate = base_coordinate + pos_m['offset'] - return self.coordinate_to_coding(base_coordinate) - def _coordinate_to_coding(self, coordinate: int) -> dict: """Convert a coordinate to a coding position (c./r.). @@ -199,9 +189,8 @@ def coding_to_coordinate(self, pos_m: dict) -> int: :returns int: Coordinate. """ - normalized_pos_m = self._normalize_position(pos_m) - return self._coding_to_coordinate(normalized_pos_m) + return self._coding_to_coordinate(pos_m) def coordinate_to_protein(self, coordinate: int) -> dict: """Convert a coordinate to a protein position (p.). diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 2f67777..a60e6da 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -82,13 +82,14 @@ def to_coordinate(self, pos_m: dict) -> int: """ region = pos_m['region'] - if pos_m['region'] in ('u', 'd'): - is_upstream = region == 'u' + if region == 'u': if self._inverted: - is_upstream = not is_upstream - if is_upstream: - return self._locations[0][0] - abs(pos_m['position']) + pos_m['offset'] - return abs(pos_m['position']) + self._locations[-1][1] + pos_m['offset'] - 1 + return self._locations[-1][1] + abs(pos_m['position']) - pos_m['offset'] - 1 + return self._locations[0][0] - abs(pos_m['position']) + pos_m['offset'] + elif region == 'd': + if self._inverted: + return self._locations[0][0] - abs(pos_m['position']) - pos_m['offset'] + return self._locations[-1][1] + abs(pos_m['position']) + pos_m['offset'] - 1 index = min( len(self._offsets), From c5d773254d8081db3303a16516af28fec599723d Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 25 Mar 2026 17:26:22 +0100 Subject: [PATCH 102/236] Fix test --- tests/test_multi_locus.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index 259eafa..99c7e50 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -312,7 +312,7 @@ def test_MultiLocus_inverted_degenerate(): multi_locus.to_coordinate, 72, [ - {'position': 0, 'offset': 1, 'region': 'u'}, + {'position': 0, 'offset': -1, 'region': 'u'}, {'position': -1, 'offset': 0, 'region': ''}, {'position': 0, 'offset': -1, 'region': ''}, {'position': 1, 'offset': 0, 'region': 'u'}, @@ -323,9 +323,9 @@ def test_MultiLocus_inverted_degenerate(): multi_locus.to_coordinate, 4, [ - {'position': 0, 'offset': -1, 'region': 'd'}, {'position': 21, 'offset': 1, 'region': ''}, {'position': 22, 'offset': 0, 'region': ''}, {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 0, 'offset': 1, 'region': 'd'}, ], ) From 49ecb5f849b65d2e3403cb9b51646a4364f5cc99 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 26 Mar 2026 12:34:19 +0100 Subject: [PATCH 103/236] Add degenerate for NonCoding --- mutalyzer_crossmapper/crossmapper.py | 48 +++++++++++----------------- tests/test_crossmapper.py | 6 ++++ 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 6ad0140..2838da7 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -85,33 +85,6 @@ def __init__(self, locations: list[tuple[int,int]], cds: tuple[int,int], inverte self._coding = (b0['position'] + b0['offset'], b1['position'] + b1['offset'] + 1) self._exons = (e0['position'], e1['position']) - def _degenerate_position(self, pos_m: dict) -> dict: - """Degenerate a coding position model (c./r.). - - :arg dict pos_m: Coding position model. - - :returns dict: a generate coding position model. - """ - region = pos_m['region'] - if region not in ('u', 'd'): - return pos_m - - degenerate_pos_m = {'offset': pos_m['offset']} - location = pos_m['position'] - if region == 'u': - if self._inverted: - degenerate_pos_m['position'] = location + self._exons[1] - self._coding[1] + 1 - else: - degenerate_pos_m['position'] = location + self._coding[0] - degenerate_pos_m['region'] = '-' - if region == 'd': - if self._inverted: - degenerate_pos_m['position'] = location + self._coding[0] - else: - degenerate_pos_m['position'] = location + self._exons[1]- self._coding[1] + 1 - degenerate_pos_m['region'] = '*' - return degenerate_pos_m - def _coordinate_to_coding(self, coordinate: int) -> dict: """Convert a coordinate to a coding position (c./r.). @@ -154,10 +127,25 @@ def coordinate_to_coding(self, coordinate: tuple[int, int], degenerate: bool=Fal """ pos_m = self._coordinate_to_coding(coordinate) - if degenerate: - pos_m = self._degenerate_position(pos_m) + region = pos_m['region'] + if not degenerate or region =='': + return pos_m - return pos_m + degenerate_pos_m = {'offset': pos_m['offset']} + location = pos_m['position'] + if region == 'u': + if self._inverted: + degenerate_pos_m['position'] = location + self._exons[1] - self._coding[1] + 1 + else: + degenerate_pos_m['position'] = location + self._coding[0] + degenerate_pos_m['region'] = '-' + if region == 'd': + if self._inverted: + degenerate_pos_m['position'] = location + self._coding[0] + else: + degenerate_pos_m['position'] = location + self._exons[1]- self._coding[1] + 1 + degenerate_pos_m['region'] = '*' + return degenerate_pos_m def _coding_to_coordinate(self, pos_m: dict) -> int: """Convert a coding position (c./r.) to a coordinate. diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 7265507..8f40840 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -107,6 +107,8 @@ def test_NonCoding_degenerate(): [ {'position': 1, 'offset': -1, 'region': ''}, {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': 0, 'region': '-'}, + {'position': 2, 'offset': 1, 'region': '-'}, ], ) @@ -118,6 +120,8 @@ def test_NonCoding_degenerate(): {'position': 1, 'offset': 0, 'region': 'd'}, {'position': 22, 'offset': 1, 'region': ''}, {'position': 23, 'offset': 0, 'region': ''}, + {'position': 24, 'offset': -1, 'region': ''}, + {'position': 1, 'offset': 0, 'region': '*'}, ], ) @@ -133,6 +137,7 @@ def test_NonCoding_inverted_degenerate(): [ {'position': 1, 'offset': -1, 'region': ''}, {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': 0, 'region': '-'}, ], ) @@ -142,6 +147,7 @@ def test_NonCoding_inverted_degenerate(): 4, [ {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 1, 'offset': 0, 'region': '*'}, {'position': 23, 'offset': 0, 'region': ''}, {'position': 22, 'offset': 1, 'region': ''}, ], From 78491956aeedafbc359b563b6578eb5bc8e5021e Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 26 Mar 2026 15:46:18 +0100 Subject: [PATCH 104/236] Add tests for degenerate in NonCoding --- mutalyzer_crossmapper/crossmapper.py | 26 +++++++++------ tests/test_crossmapper.py | 47 +++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 2838da7..2fbe449 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -33,7 +33,7 @@ def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> self._noncoding = MultiLocus(locations, inverted) - def coordinate_to_noncoding(self, coordinate: int) -> dict: + def coordinate_to_noncoding(self, coordinate: int, degenerate: bool=False) -> dict: """Convert a coordinate to a noncoding position (n./r.). :arg int coordinate: Coordinate. @@ -41,11 +41,19 @@ def coordinate_to_noncoding(self, coordinate: int) -> dict: :returns dict: Noncoding position model. """ pos_m = self._noncoding.to_position(coordinate) - if pos_m['region'] == '': + region = pos_m['region'] + if region == '': pos_m['position'] = pos_m['position'] + 1 + return pos_m + + if degenerate: + if region == 'u': + pos_m["region"] = '-' + elif region == 'd': + pos_m['region'] = '*' return pos_m - def noncoding_to_coordinate(self, pos_m: dict, degenerate: bool=True) -> int: + def noncoding_to_coordinate(self, pos_m: dict) -> int: """Convert a noncoding position (n./r.) to a coordinate. :arg dict pos_m: Noncoding position model. @@ -53,13 +61,13 @@ def noncoding_to_coordinate(self, pos_m: dict, degenerate: bool=True) -> int: :returns int: Coordinate. """ multilocus_pos_m = {**pos_m} - if degenerate: - if multilocus_pos_m["region"] == '-': - multilocus_pos_m["region"] = 'u' - elif multilocus_pos_m["region"] == '*': - multilocus_pos_m['region'] = 'd' - if multilocus_pos_m['region'] == '': + region = multilocus_pos_m['region'] + if region == '': multilocus_pos_m['position'] = pos_m['position'] - 1 + elif region == '-': + multilocus_pos_m['region'] = 'u' + elif region == '*': + multilocus_pos_m['region'] = 'd' return self._noncoding.to_coordinate(multilocus_pos_m) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 8f40840..33aec12 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -154,6 +154,52 @@ def test_NonCoding_inverted_degenerate(): ) +def test_NonCoding_degenerate_return(): + crossmap = NonCoding(_exons) + + assert crossmap.coordinate_to_noncoding(4, True) == { + 'position': 1, + 'offset': 0, + 'region': '-', + } + + assert crossmap.coordinate_to_noncoding(72, True) == { + 'position': 1, + 'offset': 0, + 'region': '*', + } + + +def test_NonCoding_inverted_degenerate_return(): + crossmap = NonCoding(_exons, True) + + assert crossmap.coordinate_to_noncoding(72, True) == { + 'position': 1, + 'offset': 0, + 'region': '-', + } + + assert crossmap.coordinate_to_noncoding(4, True) == { + 'position': 1, + 'offset': 0, + 'region': '*', + } + + +def test_NonCoding_degenerate_no_return(): + """Degenerate internal positions do not exist.""" + crossmap = NonCoding(_exons) + + assert crossmap.coordinate_to_noncoding(25) == crossmap.coordinate_to_noncoding(25, True) + + +def test_NonCoding_inverted_degenerate_no_return(): + """Degenerate internal positions do not exist.""" + crossmap = NonCoding(_exons, True) + + assert crossmap.coordinate_to_noncoding(25) == crossmap.coordinate_to_noncoding(25, True) + + def test_Coding(): """Forward oriented coding transcript.""" crossmap = Coding(_exons, _cds) @@ -544,7 +590,6 @@ def test_Coding_inverted_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" crossmap = Coding([(10, 20)], (11, 19), True) - assert crossmap.coordinate_to_coding(20, True) == { 'position': 2, 'offset': 0, From 7c3cdde5d1efd9b399fddd6904e9f592f2b847e6 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 26 Mar 2026 15:46:48 +0100 Subject: [PATCH 105/236] Update document --- docs/library.rst | 161 +++++++++++++++++++++++++---------------------- 1 file changed, 86 insertions(+), 75 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index a069719..564d9cd 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -18,11 +18,11 @@ They are represented as 1-key dictionaries. Below is an example of ``g.1`` in HG .. code-block:: python - {'position':1} + {'position': 1} Where: -- **position**: an integer repersenting a base position (>0) +- **position**: an integer representing a nucleotide position (>0) Genomic Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -38,8 +38,8 @@ used to convert to and from genomic positions. .. code:: python >>> crossmap.coordinate_to_genomic(0) - 1 - >>> crossmap.genomic_to_coordinate({'position':1}) + {'position': 1} + >>> crossmap.genomic_to_coordinate({'position': 1}) 0 See section :doc:`api/crossmap` for a detailed description. @@ -68,11 +68,11 @@ as 3-key dictionaries. Below is an example of ``n.14+1`` in HGVS. Where: -- **position**: an interger representing a transcript position (>0) +- **position**: an integer representing a transcript position (>0) - **offset**: an integer indicating the offset relative to the position (negative for upstream, positive for downstream) -- **region**: a string describing the region type (``''`` for standard, ``'u'`` for upstream, - ``'d'`` for downstream) +- **region**: a string describing the region type (empty for positions within a non-coding transcript, ``u`` for upstream, + ``d`` for downstream) NonCoding Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -92,24 +92,35 @@ position ``n.14+1``. We can convert between these two as follows. .. code:: python >>> crossmap.coordinate_to_noncoding(35) - {'position':14, 'offset':1, 'region':''} - >>> crossmap.noncoding_to_coordinate({'position':14, 'offset':1, 'region':''}) - {'position':14, 'offset':1, 'region':''} + {'position': 14, 'offset': 1, 'region': ''} + >>> crossmap.noncoding_to_coordinate({'position': 14, 'offset': 1, 'region': ''}) + 35 -When the coordinate is upstream or downstream of the transcript, we use ``'u'`` to -present upstream and ``'d'`` to present downstream. +When the coordinate is upstream or downstream of the transcript, we use ``u`` to +present upstream and ``d`` to present downstream. .. code:: python >>> crossmap.coordinate_to_noncoding(2) - {'position':3, 'offset':0, 'region':'u'} - >>> crossmap.noncoding_to_coordinate({'position':3, 'offset':0, 'region':'u'}) + {'position': 3, 'offset': 0, 'region': 'u'} + >>> crossmap.noncoding_to_coordinate({'position': 3, 'offset': 0, 'region': 'u'}) 2 >>> crossmap.coordinate_to_noncoding(73) - {'position':2, 'offset':0, 'region':'d'} - >>> crossmap.noncoding_to_coordinate({'position':2, 'offset':0, 'region':'d'}) + {'position': 2, 'offset': 0, 'region': 'd'} + >>> crossmap.noncoding_to_coordinate({'position': 2, 'offset': 0, 'region': 'd'}) 73 +The ``coordinate_to_noncoding()`` function accepts an optional ``degenerate`` +argument. When set to ``True``, positions outside of the transcript are no +longer described using the ``u`` or ``d`` notation, ``-`` and ``*``are used +instead. + +.. code:: python + + >>> crossmap.coordinate_to_noncoding(2) + {'position': 3, 'offset': 0, 'region': 'u'} + >>> crossmap.coordinate_to_noncoding(2, True) + {'position': 3, 'offset': 0, 'region': '-'} For transcripts that reside on the reverse complement strand, the ``inverted`` parameter should be set to ``True``. In our example, HGVS position ``g.36`` @@ -119,8 +130,8 @@ parameter should be set to ``True``. In our example, HGVS position ``g.36`` >>> crossmap = NonCoding(exons, inverted=True) >>> crossmap.coordinate_to_noncoding(35) - {'position':9, 'offset':-1, 'region':''} - >>> crossmap.noncoding_to_coordinate({'position':9, 'offset':-1, 'region':''}) + {'position': 9, 'offset': -1, 'region': ''} + >>> crossmap.noncoding_to_coordinate({'position': 9, 'offset': -1, 'region': ''}) 35 In the following table, we show a number of annotated examples. @@ -137,12 +148,12 @@ In the following table, we show a number of annotated examples. * - 0 - 5 - 0 - - ``'u'`` + - ``u`` - ``n.u5`` * - 4 - 1 - 0 - - ``'u'`` + - ``u`` - ``n.u1`` * - 5 - 1 @@ -167,12 +178,12 @@ In the following table, we show a number of annotated examples. * - 72 - 1 - 0 - - ``'d'`` + - ``d`` - ``n.d1`` * - 79 - 8 - 0 - - ``'d'`` + - ``d`` - ``n.d8`` See section :doc:`api/crossmap` for a detailed description. @@ -186,7 +197,7 @@ positioning systems should be done via a coordinate. Coding Position Model ~~~~~~~~~~~~~~~~~~~~~ -Coding positions follow the HGVS ``c`` coordinate system. They are +Coding positions follow the HGVS ``c.`` coordinate system. They are represented as 3-key dictionaries. Here is an example of ``c.*1+3``. .. code-block:: python @@ -199,11 +210,11 @@ represented as 3-key dictionaries. Here is an example of ``c.*1+3``. Where: -- **position**: an interger representing a transcript position (>0) +- **position**: an integer representing a transcript position (>0) - **offset**: an integer indicating the offset relative to the position (negative for upstream, positive for downstream) -- **region**: a string describing the region type (``''`` for standard coding positions, - ``'-'`` for 5' UTR, ``'*'`` for 3' UTR, ``'u'`` for upstream and ``'d'`` for downstream) +- **region**: a string describing the region type (empty for positions within coding DNA sequence, + ``-`` for 5' UTR, ``*`` for 3' UTR, ``u`` for upstream and ``d`` for downstream) Coding Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -225,21 +236,21 @@ position ``c.-1``. We can convert between these two as follows. .. code:: python >>> crossmap.coordinate_to_coding(31) - {'position':1, 'offset':0, 'region':'-'} - >>> crossmap.coding_to_coordinate({'position':1, 'offset':0, 'region':'-'}) + {'position': 1, 'offset': 0, 'region': '-'} + >>> crossmap.coding_to_coordinate({'position': 1, 'offset': 0, 'region': '-'}) 31 The ``coordinate_to_coding()`` function accepts an optional ``degenerate`` argument. When set to ``True``, positions outside of the transcript are no -longer described using the ``'u'`` or ``'d'`` notation, ``'-'`` and ``'*'`` -are used instead. +longer described using the ``u`` or ``d`` notation, ``-`` and ``*``are used +instead. Note that the value of `position` is adjusted accordingly. .. code:: python >>> crossmap.coordinate_to_coding(4) - {'position':1, 'offset':0, 'region':'u'} + {'position': 1, 'offset': 0, 'region': 'u'} >>> crossmap.coordinate_to_coding(4, True) - {'position':12, 'offset':0, 'region':'-'} + {'position': 12, 'offset': 0, 'region': '-'} In the following table, we show a number of annotated examples. @@ -255,27 +266,27 @@ In the following table, we show a number of annotated examples. * - 0 - 5 - 0 - - ``'u'`` + - ``u`` - ``c.u5`` * - 4 - 1 - 0 - - ``'u'`` + - ``u`` - ``c.u1`` * - 5 - 11 - 0 - - ``'-'`` + - ``-`` - ``c.-11`` * - 24 - 3 - 5 - - ``'-'`` + - ``-`` - ``c.-3+5`` * - 31 - 1 - 0 - - ``'-'`` + - ``-`` - ``c.-1`` * - 32 - 1 @@ -295,27 +306,27 @@ In the following table, we show a number of annotated examples. * - 43 - 1 - 0 - - ``'*'`` + - ``*`` - ``c.*1`` * - 61 - 4 - -9 - - ``'*'`` + - ``*`` - ``c.*4-9`` * - 71 - 5 - 0 - - ``'*'`` + - ``*`` - ``c.*5`` * - 72 - 1 - 0 - - ``'d'`` + - ``d`` - ``c.d1`` * - 79 - 8 - 0 - - ``'d'`` + - ``d`` - ``c.d8`` @@ -324,24 +335,24 @@ Protein Additionally, the functions ``coordinate_to_protein()`` and ``protein_to_coordinate()`` can be used. These functions use a 4-key dictionary -to represent a protein position. Here is an example of ``p.1`` in HGVS. +to represent a protein position. Here is one example of three posibilities +for ``p.1`` in HGVS. .. code-block:: python { 'position': 1, 'position_in_codon': 3, - 'offset': 3, + 'offset': 0, 'region': '' } Where: -- **position**: an interger representing the protein position (>0) +- **position**: an integer representing an amino acid position (>0) - **position_in_codon**: an integer indicating the nucleotide index within the codon (1, 2, or 3) -- **offset**: an integer indicating offset relative to the codon -- **region**: a string describing the region type (``''`` for standard positions) - +- **offset**: an integer indicating offset relative to the nucleotide specified by `position_in_codon` in the codon +- **region**: a string describing the region type (empty for vaid amino acid positions) In our example the HGVS position ``g.42`` (coordinate `41`) corresponds with position ``p.2``. We can convert between these to as follows. @@ -349,12 +360,12 @@ position ``p.2``. We can convert between these to as follows. .. code:: python >>> crossmap.coordinate_to_protein(41) - {'position':2, 'position_in_codon':2, 'offset':0, 'region':''} - >>> crossmap.protein_to_coordinate({'position':2, 'position_in_codon':2, 'offset':0, 'region':''}) + {'position': 2, 'position_in_codon': 2, 'offset': 0, 'region': ''} + >>> crossmap.protein_to_coordinate({'position': 2, 'position_in_codon': 2, 'offset': 0, 'region': ''}) 41 **Note:** protein position only corresponds with the HGVS "p." notation -when the offset equals ``0`` and the region equals ``''``. In the following +when the offset equals ``0`` and the region equals empty. In the following table, we show a number of annotated examples. .. _table_protein: @@ -371,26 +382,26 @@ table, we show a number of annotated examples. - 2 - 2 - 0 - - ``'u'`` - - invalid + - ``u`` + - * - 4 - 1 - 3 - 0 - - ``'u'`` - - invalid + - ``u`` + - * - 5 - 4 - 2 - 0 - - ``'-'`` - - invalid + - ``-`` + - * - 31 - 1 - 3 - 0 - - ``'-'`` - - invalid + - ``-`` + - * - 32 - 1 - 1 @@ -414,7 +425,7 @@ table, we show a number of annotated examples. - 3 - 1 - - - ``p.1`` + - * - 42 - 2 - 3 @@ -425,27 +436,27 @@ table, we show a number of annotated examples. - 1 - 1 - 0 - - ``'*'`` - - invalid + - ``*`` + - * - 44 - 1 - 1 - 1 - - ``'*'`` - - invalid + - ``*`` + - * - 72 - 1 - 1 - 0 - - ``'d'`` - - invalid + - ``d`` + - * - 79 - 2 - 2 - 0 - - ``'d'`` - - invalid + - ``d`` + - See section :doc:`api/crossmap` for a detailed description. @@ -506,9 +517,9 @@ This differs from HGVS numbering, which is **1-based**. .. code:: python >>> locus.to_position(9) - {'position':0, 'offset':-1} - >>> locus.to_coordinate({'position':0, 'offset':-1}) - {'position':0, 'offset':-1} + {'position': 0, 'offset': -1} + >>> locus.to_coordinate({'position': 0, 'offset': -1}) + 9 For loci that reside on the reverse complement strand, the optional ``inverted`` constructor parameter should be set to ``True``. @@ -534,12 +545,12 @@ The interface to this class is similar to that of the ``Locus`` class. Functions .. code:: python >>> multilocus.to_position(22) - {'position':9, 'offset':3, 'region':''} - >>> multilocus.to_coordinate({'position':9, 'offset':3, 'region':''}) + {'position': 9, 'offset': 3, 'region': ''} + >>> multilocus.to_coordinate({'position': 9, 'offset': 3, 'region': ''}) 22 >>> multilocus.to_position(38) - {'position':10, 'offset':-2, 'region':''} - >>> multilocus.to_coordinate({'position':10, 'offset':-2, 'region':''} + {'position': 10, 'offset': -2, 'region': ''} + >>> multilocus.to_coordinate({'position': 10, 'offset': -2, 'region': ''}) 38 See section :doc:`api/multi_locus` for a detailed description. From 651d1700881835eed4f6b34a5ae4faf32c957165 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 26 Mar 2026 15:56:28 +0100 Subject: [PATCH 106/236] Formatting --- README.rst | 14 +++++++------- docs/library.rst | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index fd0308c..ebd1ed6 100644 --- a/README.rst +++ b/README.rst @@ -53,8 +53,8 @@ positions and coordinates. >>> from mutalyzer_crossmapper import Genomic >>> crossmap = Genomic() >>> crossmap.coordinate_to_genomic(0) - 1 - >>> crossmap.genomic_to_coordinate({'position':1}) + {'position': 1} + >>> crossmap.genomic_to_coordinate({'position': 1}) 0 On top of the functionality provided by the ``Genomic`` class, the @@ -67,8 +67,8 @@ positions and coordinates. >>> exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] >>> crossmap = NonCoding(exons) >>> crossmap.coordinate_to_noncoding(35) - {'position':14, 'offset':1, 'region':''} - >>> crossmap.noncoding_to_coordinate({'position':14, 'offset':1, 'region':''}) + {'position': 14, 'offset': 1, 'region': ''} + >>> crossmap.noncoding_to_coordinate({'position': 14, 'offset': 1, 'region': ''}) 35 Add the flag ``inverted=True`` to the constructor when the transcript resides @@ -84,7 +84,7 @@ coordinates as well as conversions between protein positions and coordinates. >>> cds = (32, 43) >>> crossmap = Coding(exons, cds) >>> crossmap.coordinate_to_coding(31) - {'position':1, 'offset':0, 'region':'-'} + {'position': 1, 'offset': 0, 'region': '-'} >>> crossmap.coding_to_coordinate({'position':1, 'offset':0, 'region':'-'}) 31 @@ -96,8 +96,8 @@ Conversions between protein positions and coordinates are done as follows. .. code:: python >>> crossmap.coordinate_to_protein(41) - {'position':2, 'position_in_codon': 2, 'offset':0, 'region':''} - >>> crossmap.protein_to_coordinate({'position':2, 'position_in_codon':2, 'offset':0, 'region':''}) + {'position': 2, 'position_in_codon': 2, 'offset': 0, 'region': ''} + >>> crossmap.protein_to_coordinate({'position': 2, 'position_in_codon': 2, 'offset': 0, 'region': ''}) 41 diff --git a/docs/library.rst b/docs/library.rst index 564d9cd..bc982a0 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -8,7 +8,7 @@ The ``Genomic`` class --------------------- The ``Genomic`` class provides an interface to conversions between genomic -(``g.``, ``m.``, ``n.``) positions and coordinates. +(``g.``, ``m.``, ``o.``) positions and coordinates. Genomic Position Model ~~~~~~~~~~~~~~~~~~~~~~~ From 21f84582e8ae16f04706a7f22265f4ed0455c63d Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:08:51 +0100 Subject: [PATCH 107/236] Update library.rst --- docs/library.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index bc982a0..4264ff9 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -71,8 +71,8 @@ Where: - **position**: an integer representing a transcript position (>0) - **offset**: an integer indicating the offset relative to the position (negative for upstream, positive for downstream) -- **region**: a string describing the region type (empty for positions within a non-coding transcript, ``u`` for upstream, - ``d`` for downstream) +- **region**: a string describing the region type (empty for positions within a non-coding + transcript, ``u`` for upstream, ``d`` for downstream) NonCoding Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -112,7 +112,7 @@ present upstream and ``d`` to present downstream. The ``coordinate_to_noncoding()`` function accepts an optional ``degenerate`` argument. When set to ``True``, positions outside of the transcript are no -longer described using the ``u`` or ``d`` notation, ``-`` and ``*``are used +longer described using the ``u`` or ``d`` notation, ``-`` and ``*`` are used instead. .. code:: python @@ -242,7 +242,7 @@ position ``c.-1``. We can convert between these two as follows. The ``coordinate_to_coding()`` function accepts an optional ``degenerate`` argument. When set to ``True``, positions outside of the transcript are no -longer described using the ``u`` or ``d`` notation, ``-`` and ``*``are used +longer described using the ``u`` or ``d`` notation, ``-`` and ``*`` are used instead. Note that the value of `position` is adjusted accordingly. .. code:: python From 34d084479060863c305b580edf7d7d9f7e764d8d Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 26 Mar 2026 16:21:40 +0100 Subject: [PATCH 108/236] Fix typo --- docs/library.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 4264ff9..ec1bdc2 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -71,7 +71,7 @@ Where: - **position**: an integer representing a transcript position (>0) - **offset**: an integer indicating the offset relative to the position (negative for upstream, positive for downstream) -- **region**: a string describing the region type (empty for positions within a non-coding +- **region**: a string describing the region type (empty for positions within a non-coding transcript, ``u`` for upstream, ``d`` for downstream) NonCoding Position Conversion @@ -243,7 +243,7 @@ position ``c.-1``. We can convert between these two as follows. The ``coordinate_to_coding()`` function accepts an optional ``degenerate`` argument. When set to ``True``, positions outside of the transcript are no longer described using the ``u`` or ``d`` notation, ``-`` and ``*`` are used -instead. Note that the value of `position` is adjusted accordingly. +instead. Note that the value of ``position`` is adjusted accordingly. .. code:: python From 8873a779365da276273a33c7e24e764f467ba166 Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:23:16 +0100 Subject: [PATCH 109/236] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 324acae..381420f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,3 @@ docs/_build/ mutalyzer_crossmapper.egg-info/ mutalyzer_crossmapper/__pycache__/ tests/__pycache__/ -tmp \ No newline at end of file From c9e58c67df7c26e82c73c4d9e3fcf329d3d7ff4f Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 27 Mar 2026 09:44:09 +0100 Subject: [PATCH 110/236] Set upstream and downstream in multilocus as 0 based --- mutalyzer_crossmapper/crossmapper.py | 24 +++++++++++++----------- mutalyzer_crossmapper/multi_locus.py | 10 +++++----- tests/test_multi_locus.py | 20 ++++++++------------ 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 2fbe449..63cee23 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -42,8 +42,8 @@ def coordinate_to_noncoding(self, coordinate: int, degenerate: bool=False) -> di """ pos_m = self._noncoding.to_position(coordinate) region = pos_m['region'] + pos_m['position'] = pos_m['position'] + 1 if region == '': - pos_m['position'] = pos_m['position'] + 1 return pos_m if degenerate: @@ -62,9 +62,8 @@ def noncoding_to_coordinate(self, pos_m: dict) -> int: """ multilocus_pos_m = {**pos_m} region = multilocus_pos_m['region'] - if region == '': - multilocus_pos_m['position'] = pos_m['position'] - 1 - elif region == '-': + multilocus_pos_m['position'] = multilocus_pos_m['position'] - 1 + if region == '-': multilocus_pos_m['region'] = 'u' elif region == '*': multilocus_pos_m['region'] = 'd' @@ -103,6 +102,7 @@ def _coordinate_to_coding(self, coordinate: int) -> dict: noncoding_pos_m = self._noncoding.to_position(coordinate) if noncoding_pos_m['region'] in ('u', 'd'): + noncoding_pos_m['position'] = noncoding_pos_m['position'] + 1 return noncoding_pos_m location = noncoding_pos_m['position'] @@ -136,7 +136,7 @@ def coordinate_to_coding(self, coordinate: tuple[int, int], degenerate: bool=Fal pos_m = self._coordinate_to_coding(coordinate) region = pos_m['region'] - if not degenerate or region =='': + if not degenerate or region == '': return pos_m degenerate_pos_m = {'offset': pos_m['offset']} @@ -164,19 +164,21 @@ def _coding_to_coordinate(self, pos_m: dict) -> int: """ location = pos_m['position'] region = pos_m['region'] + multilocus_pos_m = {**pos_m} if region in ('u', 'd'): - return self._noncoding.to_coordinate(pos_m) + multilocus_pos_m['position'] = location - 1 + return self._noncoding.to_coordinate(multilocus_pos_m) - noncoding_pos_m = {'offset': pos_m['offset'], 'region': ''} + multilocus_pos_m['region'] = '' if region == '': - noncoding_pos_m['position'] = location + self._coding[0] - 1 + multilocus_pos_m['position'] = location + self._coding[0] - 1 elif region == '-': - noncoding_pos_m['position'] = self._coding[0] - location + multilocus_pos_m['position'] = self._coding[0] - location else: - noncoding_pos_m['position'] = self._coding[1] + location - 1 + multilocus_pos_m['position'] = self._coding[1] + location - 1 - return self._noncoding.to_coordinate(noncoding_pos_m) + return self._noncoding.to_coordinate(multilocus_pos_m) def coding_to_coordinate(self, pos_m: dict) -> int: """Convert a coding position (c./r.) to a coordinate. diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index a60e6da..6984c97 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -63,7 +63,7 @@ def to_position(self, coordinate: int) -> dict[str: int | str]: if outside: return { - 'position': abs(locus_pos_m['offset']), + 'position': abs(locus_pos_m['offset']) - 1, 'offset': 0, 'region': region } @@ -84,12 +84,12 @@ def to_coordinate(self, pos_m: dict) -> int: if region == 'u': if self._inverted: - return self._locations[-1][1] + abs(pos_m['position']) - pos_m['offset'] - 1 - return self._locations[0][0] - abs(pos_m['position']) + pos_m['offset'] + return self._locations[-1][1] + abs(pos_m['position']) - pos_m['offset'] + return self._locations[0][0] - abs(pos_m['position']) + pos_m['offset'] - 1 elif region == 'd': if self._inverted: - return self._locations[0][0] - abs(pos_m['position']) - pos_m['offset'] - return self._locations[-1][1] + abs(pos_m['position']) + pos_m['offset'] - 1 + return self._locations[0][0] - abs(pos_m['position']) - pos_m['offset'] - 1 + return self._locations[-1][1] + abs(pos_m['position']) + pos_m['offset'] index = min( len(self._offsets), diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index 99c7e50..9725ad7 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -35,7 +35,7 @@ def test_MultiLocus(): multi_locus.to_position, 4, multi_locus.to_coordinate, - {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 0, 'offset': 0, 'region': 'u'}, ) invariant( @@ -94,7 +94,7 @@ def test_MultiLocus(): multi_locus.to_position, 72, multi_locus.to_coordinate, - {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 0, 'offset': 0, 'region': 'd'}, ) @@ -107,7 +107,7 @@ def test_MultiLocus_inverted(): multi_locus.to_position, 72, multi_locus.to_coordinate, - {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 0, 'offset': 0, 'region': 'u'}, ) invariant( multi_locus.to_position, @@ -165,7 +165,7 @@ def test_MultiLocus_inverted(): multi_locus.to_position, 4, multi_locus.to_coordinate, - {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 0, 'offset': 0, 'region': 'd'}, ) @@ -287,8 +287,7 @@ def test_MultiLocus_degenerate(): [ {'position': 0, 'offset': -1, 'region': ''}, {'position': -1, 'offset': 0, 'region': ''}, - {'position': 1, 'offset': 0, 'region': 'u'}, - {'position': -1, 'offset': 0, 'region': 'u'}, + {'position': 0, 'offset': 0, 'region': 'u'}, ], ) @@ -298,8 +297,7 @@ def test_MultiLocus_degenerate(): [ {'position': 21, 'offset': 1, 'region': ''}, {'position': 22, 'offset': 0, 'region': ''}, - {'position': 0, 'offset': 1, 'region': 'd'}, - {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 0, 'offset': 0, 'region': 'd'}, ], ) @@ -312,10 +310,9 @@ def test_MultiLocus_inverted_degenerate(): multi_locus.to_coordinate, 72, [ - {'position': 0, 'offset': -1, 'region': 'u'}, {'position': -1, 'offset': 0, 'region': ''}, {'position': 0, 'offset': -1, 'region': ''}, - {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 0, 'offset': 0, 'region': 'u'}, ], ) @@ -325,7 +322,6 @@ def test_MultiLocus_inverted_degenerate(): [ {'position': 21, 'offset': 1, 'region': ''}, {'position': 22, 'offset': 0, 'region': ''}, - {'position': 1, 'offset': 0, 'region': 'd'}, - {'position': 0, 'offset': 1, 'region': 'd'}, + {'position': 0, 'offset': 0, 'region': 'd'}, ], ) From 783223d90ef46422bc14d4e016ad2e06c2ea53a7 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 27 Mar 2026 11:09:03 +0100 Subject: [PATCH 111/236] Fix typing mistake --- mutalyzer_crossmapper/crossmapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 63cee23..6c5a7be 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -125,7 +125,7 @@ def _coordinate_to_coding(self, coordinate: int) -> dict: 'region': '' } - def coordinate_to_coding(self, coordinate: tuple[int, int], degenerate: bool=False) -> dict: + def coordinate_to_coding(self, coordinate: int, degenerate: bool=False) -> dict: """Convert a coordinate to a coding position (c./r.). :arg int coordinate: Coordinate. From a7196f4a6fb7bbfff0e412a937b8e1428b19a563 Mon Sep 17 00:00:00 2001 From: "X.Liu" <34545147+XLIU-hub@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:57:10 +0100 Subject: [PATCH 112/236] Use importlib instead of pkg_resources --- mutalyzer_crossmapper/__init__.py | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/mutalyzer_crossmapper/__init__.py b/mutalyzer_crossmapper/__init__.py index 284f6c8..7d1abc0 100644 --- a/mutalyzer_crossmapper/__init__.py +++ b/mutalyzer_crossmapper/__init__.py @@ -1,18 +1,4 @@ -"""Crossmapper position conversion library. - -Definitions: - -- Coordinates are zero based, non-negative integers. -- Locations are zero based right-open non-negative integer intervals, - consistent with Python's range() and sequence slicing functions. -- Loci and exons are locations. -- An exon list is a list of locations that, when flattened, is an increasing - sequence. -- A position is a 2-tuple of which the first element is a one based non-zero - integer relative to an element in a location and the second element is an - integer offset relative to the first element. -""" -from pkg_resources import get_distribution +from importlib.metadata import metadata from .crossmapper import Coding, Genomic, NonCoding from .location import nearest_location @@ -20,14 +6,13 @@ from .multi_locus import MultiLocus -def _get_metadata(name): - pkg = get_distribution('mutalyzer_crossmapper') - - for line in pkg.get_metadata_lines(pkg.PKG_INFO): - if line.startswith('{}: '.format(name)): - return line.split(': ')[1] - - return '' +def _get_metadata(name: str) -> str: + """Get metadata from the package using importlib.metadata""" + try: + meta = metadata('mutalyzer_crossmapper') + return meta.get(name, '') + except Exception: + return '' _copyright_notice = 'Copyright (c) {} <{}>'.format( From 88ce74f5bcc0f66d4f074e84fbe43752d7ddab64 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 30 Mar 2026 11:40:58 +0200 Subject: [PATCH 113/236] Fix typing and single quote --- mutalyzer_crossmapper/crossmapper.py | 31 +++++++++++++++------------- mutalyzer_crossmapper/locus.py | 4 ++-- mutalyzer_crossmapper/multi_locus.py | 4 ++-- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 6c5a7be..ac4ef65 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -24,7 +24,7 @@ def genomic_to_coordinate(self, pos_m: dict) -> int: class NonCoding(Genomic): """NonCoding crossmap object.""" - def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> None: + def __init__(self, locations: list[tuple[int, int]], inverted: bool=False) -> None: """ :arg list locations: List of locus locations. :arg bool inverted: Orientation. @@ -48,31 +48,34 @@ def coordinate_to_noncoding(self, coordinate: int, degenerate: bool=False) -> di if degenerate: if region == 'u': - pos_m["region"] = '-' + pos_m['region'] = '-' elif region == 'd': pos_m['region'] = '*' return pos_m - def noncoding_to_coordinate(self, pos_m: dict) -> int: + def noncoding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: """Convert a noncoding position (n./r.) to a coordinate. :arg dict pos_m: Noncoding position model. :returns int: Coordinate. """ - multilocus_pos_m = {**pos_m} - region = multilocus_pos_m['region'] - multilocus_pos_m['position'] = multilocus_pos_m['position'] - 1 - if region == '-': + multilocus_pos_m = {**pos_m, 'position': pos_m['position'] - 1} + if multilocus_pos_m['region'] == '-': multilocus_pos_m['region'] = 'u' - elif region == '*': + elif multilocus_pos_m['region'] == '*': multilocus_pos_m['region'] = 'd' return self._noncoding.to_coordinate(multilocus_pos_m) class Coding(NonCoding): """Coding crossmap object.""" - def __init__(self, locations: list[tuple[int,int]], cds: tuple[int,int], inverted : bool=False) -> None: + def __init__( + self, + locations: list[tuple[int,int]], + cds: tuple[int,int], + inverted : bool=False + ) -> None: """ :arg list locations: List of locus locations. :arg tuple cds: Locus location. @@ -92,7 +95,7 @@ def __init__(self, locations: list[tuple[int,int]], cds: tuple[int,int], inverte self._coding = (b0['position'] + b0['offset'], b1['position'] + b1['offset'] + 1) self._exons = (e0['position'], e1['position']) - def _coordinate_to_coding(self, coordinate: int) -> dict: + def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: """Convert a coordinate to a coding position (c./r.). :arg int coordinate: Coordinate. @@ -155,7 +158,7 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool=False) -> dict: degenerate_pos_m['region'] = '*' return degenerate_pos_m - def _coding_to_coordinate(self, pos_m: dict) -> int: + def _coding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: """Convert a coding position (c./r.) to a coordinate. :arg dict pos_m: Coding position model (c./r.). @@ -180,7 +183,7 @@ def _coding_to_coordinate(self, pos_m: dict) -> int: return self._noncoding.to_coordinate(multilocus_pos_m) - def coding_to_coordinate(self, pos_m: dict) -> int: + def coding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: """Convert a coding position (c./r.) to a coordinate. :arg dict pos_m: Coding position model (c./r.). @@ -190,7 +193,7 @@ def coding_to_coordinate(self, pos_m: dict) -> int: return self._coding_to_coordinate(pos_m) - def coordinate_to_protein(self, coordinate: int) -> dict: + def coordinate_to_protein(self, coordinate: int) -> dict[str, int | str]: """Convert a coordinate to a protein position (p.). :arg int coordinate: Coordinate. @@ -210,7 +213,7 @@ def coordinate_to_protein(self, coordinate: int) -> dict: 'position_in_codon': (location + 2) % 3 + 1, **{k: v for k, v in pos.items() if k != 'position'}} - def protein_to_coordinate(self, pos_m: dict) -> int: + def protein_to_coordinate(self, pos_m: dict[str, int | str]) -> int: """Convert a protein position (p.) to a coordinate. :arg dict position: Protein position model(p.). diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index fff0c66..d62144d 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -1,6 +1,6 @@ class Locus(object): """Locus object.""" - def __init__(self, location: list[tuple[int, int]], inverted=False) -> None: + def __init__(self, location: tuple[int, int], inverted: bool=False) -> None: """ :arg tuple location: Locus location. :arg bool inverted: Orientation. @@ -30,7 +30,7 @@ def to_position(self, coordinate: int) -> dict[str, int]: return {'position': self._end, 'offset': coordinate - self.boundary[1]} return {'position': coordinate - self.boundary[0], 'offset': 0} - def to_coordinate(self, pos_m: dict) -> int: + def to_coordinate(self, pos_m: dict[str, int]) -> int: """Convert a position model to a coordinate. :arg dict position: Position model with 'position' and 'offset' keys. diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 6984c97..5f1cee0 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -49,7 +49,7 @@ def outside(self, coordinate: int) -> int: return coordinate - self._loci[-1].boundary[1] return 0 - def to_position(self, coordinate: int) -> dict[str: int | str]: + def to_position(self, coordinate: int) -> dict[str, int | str]: """Convert a coordinate to a position. :arg int coordinate: Coordinate. @@ -73,7 +73,7 @@ def to_position(self, coordinate: int) -> dict[str: int | str]: 'region': region } - def to_coordinate(self, pos_m: dict) -> int: + def to_coordinate(self, pos_m: dict[str, int | str]) -> int: """Convert a position model to a coordinate. :arg dict pos_m: Position model with 'position','offset' and 'region' keys. From 6d5e2827e0d4da6b127892c2ab2c272f6da7bb6a Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 30 Mar 2026 11:55:45 +0200 Subject: [PATCH 114/236] Fix doc string --- docs/library.rst | 2 +- mutalyzer_crossmapper/crossmapper.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index ec1bdc2..6b358f9 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -530,7 +530,7 @@ The ``MultiLocus`` class ^^^^^^^^^^^^^^^^^^^^^^^^ The ``MultiLocus`` class is used to deal with offsets with respect to multiple -loci. Its positions is +loci. .. code:: python diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index ac4ef65..dd0a774 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -3,7 +3,7 @@ class Genomic(object): """Genomic crossmap object.""" - def coordinate_to_genomic(self, coordinate: int) -> dict: + def coordinate_to_genomic(self, coordinate: int) -> dict[str, int]: """Convert a coordinate to a genomic position (g./m./o.). :arg int coordinate: Coordinate. @@ -12,7 +12,7 @@ def coordinate_to_genomic(self, coordinate: int) -> dict: """ return {'position': coordinate + 1} - def genomic_to_coordinate(self, pos_m: dict) -> int: + def genomic_to_coordinate(self, pos_m: dict[str, int]) -> int: """Convert a genomic position (g./m./o.) to a coordinate. :arg dict pos_m: Genomic position model. @@ -216,7 +216,7 @@ def coordinate_to_protein(self, coordinate: int) -> dict[str, int | str]: def protein_to_coordinate(self, pos_m: dict[str, int | str]) -> int: """Convert a protein position (p.) to a coordinate. - :arg dict position: Protein position model(p.). + :arg dict pos_m: Protein position model(p.). :returns int: Coordinate. """ From 8fba978d4755cdcfabf6f6f174b453eb5f104d08 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 30 Mar 2026 12:10:20 +0200 Subject: [PATCH 115/236] Update python version in setup --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index ca7be54..c58c160 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ classifiers = [options] packages = find: +python_requires = >=3.10 [options.extras_require] tests = From 2867ea8d5febc79bd86d57f1c8ca18b158a2a345 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 30 Mar 2026 14:28:54 +0200 Subject: [PATCH 116/236] Rename multilocus positon model --- mutalyzer_crossmapper/crossmapper.py | 45 ++++++++++++++-------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index dd0a774..8ece035 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -40,18 +40,18 @@ def coordinate_to_noncoding(self, coordinate: int, degenerate: bool=False) -> di :returns dict: Noncoding position model. """ - pos_m = self._noncoding.to_position(coordinate) - region = pos_m['region'] - pos_m['position'] = pos_m['position'] + 1 + multilocus_pos_m = self._noncoding.to_position(coordinate) + noncoding_pos_m = {**multilocus_pos_m, 'position': multilocus_pos_m['position'] + 1} + region = noncoding_pos_m['region'] if region == '': - return pos_m + return noncoding_pos_m if degenerate: if region == 'u': - pos_m['region'] = '-' + noncoding_pos_m['region'] = '-' elif region == 'd': - pos_m['region'] = '*' - return pos_m + noncoding_pos_m['region'] = '*' + return noncoding_pos_m def noncoding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: """Convert a noncoding position (n./r.) to a coordinate. @@ -84,16 +84,16 @@ def __init__( NonCoding.__init__(self, locations, inverted) b0 = self._noncoding.to_position(cds[0]) - b1 = self._noncoding.to_position(cds[1]-1) + b1 = self._noncoding.to_position(cds[1] - 1) e0 = self._noncoding.to_position(locations[0][0]) - e1 = self._noncoding.to_position(locations[-1][1]-1) + e1 = self._noncoding.to_position(locations[-1][1] - 1) if self._inverted: self._coding = (b1['position'] + b1['offset'], b0['position'] + b0['offset'] + 1) - self._exons = (e1['position'], e0['position']) + self._exons = (e1['position'], e0['position'] + 1) else: self._coding = (b0['position'] + b0['offset'], b1['position'] + b1['offset'] + 1) - self._exons = (e0['position'], e1['position']) + self._exons = (e0['position'], e1['position'] + 1) def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: """Convert a coordinate to a coding position (c./r.). @@ -102,14 +102,13 @@ def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: :returns dict: Coding position model (c./r.). """ - noncoding_pos_m = self._noncoding.to_position(coordinate) + multilocus_pos_m = self._noncoding.to_position(coordinate) - if noncoding_pos_m['region'] in ('u', 'd'): - noncoding_pos_m['position'] = noncoding_pos_m['position'] + 1 - return noncoding_pos_m + if multilocus_pos_m['region'] in ('u', 'd'): + return {**multilocus_pos_m, 'position': multilocus_pos_m['position'] + 1} - location = noncoding_pos_m['position'] - offset = noncoding_pos_m['offset'] + location = multilocus_pos_m['position'] + offset = multilocus_pos_m['offset'] if location < self._coding[0]: return { 'position': self._coding[0] - location, @@ -142,11 +141,11 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool=False) -> dict: if not degenerate or region == '': return pos_m - degenerate_pos_m = {'offset': pos_m['offset']} + degenerate_pos_m = {**pos_m, 'offset': pos_m['offset']} location = pos_m['position'] if region == 'u': if self._inverted: - degenerate_pos_m['position'] = location + self._exons[1] - self._coding[1] + 1 + degenerate_pos_m['position'] = location + self._exons[1] - self._coding[1] else: degenerate_pos_m['position'] = location + self._coding[0] degenerate_pos_m['region'] = '-' @@ -154,7 +153,7 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool=False) -> dict: if self._inverted: degenerate_pos_m['position'] = location + self._coding[0] else: - degenerate_pos_m['position'] = location + self._exons[1]- self._coding[1] + 1 + degenerate_pos_m['position'] = location + self._exons[1] - self._coding[1] degenerate_pos_m['region'] = '*' return degenerate_pos_m @@ -207,11 +206,13 @@ def coordinate_to_protein(self, coordinate: int) -> dict[str, int | str]: return { 'position': abs(-location // 3), 'position_in_codon': -location % 3 + 1, - **{k: v for k, v in pos.items() if k != 'position'}} + 'region': pos['region'], + 'offset': pos['offset']} return { 'position': (location + 2) // 3, 'position_in_codon': (location + 2) % 3 + 1, - **{k: v for k, v in pos.items() if k != 'position'}} + 'region': pos['region'], + 'offset': pos['offset']} def protein_to_coordinate(self, pos_m: dict[str, int | str]) -> int: """Convert a protein position (p.) to a coordinate. From 12229cd508988f85a3cca102005778cb5e76886f Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 30 Mar 2026 14:30:12 +0200 Subject: [PATCH 117/236] Remove unneccessary elif --- mutalyzer_crossmapper/multi_locus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 5f1cee0..627a31b 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -86,7 +86,7 @@ def to_coordinate(self, pos_m: dict[str, int | str]) -> int: if self._inverted: return self._locations[-1][1] + abs(pos_m['position']) - pos_m['offset'] return self._locations[0][0] - abs(pos_m['position']) + pos_m['offset'] - 1 - elif region == 'd': + if region == 'd': if self._inverted: return self._locations[0][0] - abs(pos_m['position']) - pos_m['offset'] - 1 return self._locations[-1][1] + abs(pos_m['position']) + pos_m['offset'] From c6455e907f25ffe9c0aa74906af80912bb733558 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 30 Mar 2026 15:04:05 +0200 Subject: [PATCH 118/236] Fix flake8 whitespace issues --- mutalyzer_crossmapper/crossmapper.py | 12 ++++++------ mutalyzer_crossmapper/location.py | 2 +- mutalyzer_crossmapper/locus.py | 2 +- mutalyzer_crossmapper/multi_locus.py | 2 +- tests/test_crossmapper.py | 9 +++++++++ 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 8ece035..7a38392 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -24,7 +24,7 @@ def genomic_to_coordinate(self, pos_m: dict[str, int]) -> int: class NonCoding(Genomic): """NonCoding crossmap object.""" - def __init__(self, locations: list[tuple[int, int]], inverted: bool=False) -> None: + def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> None: """ :arg list locations: List of locus locations. :arg bool inverted: Orientation. @@ -33,7 +33,7 @@ def __init__(self, locations: list[tuple[int, int]], inverted: bool=False) -> No self._noncoding = MultiLocus(locations, inverted) - def coordinate_to_noncoding(self, coordinate: int, degenerate: bool=False) -> dict: + def coordinate_to_noncoding(self, coordinate: int, degenerate: bool = False) -> dict: """Convert a coordinate to a noncoding position (n./r.). :arg int coordinate: Coordinate. @@ -72,9 +72,9 @@ class Coding(NonCoding): """Coding crossmap object.""" def __init__( self, - locations: list[tuple[int,int]], - cds: tuple[int,int], - inverted : bool=False + locations: list[tuple[int, int]], + cds: tuple[int, int], + inverted: bool = False ) -> None: """ :arg list locations: List of locus locations. @@ -127,7 +127,7 @@ def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: 'region': '' } - def coordinate_to_coding(self, coordinate: int, degenerate: bool=False) -> dict: + def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dict: """Convert a coordinate to a coding position (c./r.). :arg int coordinate: Coordinate. diff --git a/mutalyzer_crossmapper/location.py b/mutalyzer_crossmapper/location.py index 1bf7a06..9b534e4 100644 --- a/mutalyzer_crossmapper/location.py +++ b/mutalyzer_crossmapper/location.py @@ -19,7 +19,7 @@ def _nearest_boundary(lb: int, rb: int, c: int, p: int) -> int: return p -def nearest_location(ls: list[tuple[int,int]], c: int, p: int = 0) -> int: +def nearest_location(ls: list[tuple[int, int]], c: int, p: int = 0) -> int: """Find the location nearest to `c`. In case of a draw, the parameter `p` decides which index is chosen. diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index d62144d..7fbb7b3 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -1,6 +1,6 @@ class Locus(object): """Locus object.""" - def __init__(self, location: tuple[int, int], inverted: bool=False) -> None: + def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: """ :arg tuple location: Locus location. :arg bool inverted: Orientation. diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 627a31b..d2c5662 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -19,7 +19,7 @@ def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: class MultiLocus(object): """MultiLocus object.""" - def __init__(self, locations: list[tuple[int, int]], inverted: bool=False) -> None: + def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> None: """ :arg list locations: List of locus locations. :arg bool inverted: Orientation. diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 33aec12..f69ee05 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -112,6 +112,15 @@ def test_NonCoding_degenerate(): ], ) + # # Boundary between exon and intron + # degenerate_equal( + # crossmap.noncoding_to_coordinate, + # 29, + # [ + + # ] + # ) + # Boundary between downstream and transcript. degenerate_equal( crossmap.noncoding_to_coordinate, From 12e8e1f67c47d9f87c9f3c8b0ab17c7208a457c9 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 30 Mar 2026 15:40:33 +0200 Subject: [PATCH 119/236] Fix whitespaces --- tests/test_crossmapper.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index f69ee05..0ed4c47 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -112,15 +112,6 @@ def test_NonCoding_degenerate(): ], ) - # # Boundary between exon and intron - # degenerate_equal( - # crossmap.noncoding_to_coordinate, - # 29, - # [ - - # ] - # ) - # Boundary between downstream and transcript. degenerate_equal( crossmap.noncoding_to_coordinate, @@ -375,7 +366,7 @@ def test_Coding_no_utr5_inverted(): crossmap.coordinate_to_coding, 19, crossmap.coding_to_coordinate, - {'position': 1 , 'offset': 0, 'region': ''}, + {'position': 1, 'offset': 0, 'region': ''}, ) @@ -521,6 +512,7 @@ def test_Coding_degenerate(): """Degenerate upstream and downstream positions are silently corrected.""" crossmap = Coding([(10, 20)], (11, 19)) + # Degenerate position in upstream. degenerate_equal( crossmap.coding_to_coordinate, 9, From e2f2a43184023a36b38f6758bb1837fdb36b970a Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 30 Mar 2026 16:18:24 +0200 Subject: [PATCH 120/236] Add tests for protein positions from the reverse strand --- tests/test_crossmapper.py | 97 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 0ed4c47..87b5ea7 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -351,6 +351,28 @@ def test_Coding_no_utr5(): ) +def test_Coding_no_intron(): + crossmap = Coding([(10, 20), (20, 30)], (15, 25)) + + invariant( + crossmap.coordinate_to_coding, + 20, + crossmap.coding_to_coordinate, + {'position': 6, 'offset': 0, 'region': ''}, + ) + + +def test_Coding_no_intron_inverted(): + crossmap = Coding([(10, 20), (20, 30)], (15, 25), True) + + invariant( + crossmap.coordinate_to_coding, + 20, + crossmap.coding_to_coordinate, + {'position': 5, 'offset': 0, 'region': ''}, + ) + + def test_Coding_no_utr5_inverted(): """A 5' UTR may be missing.""" crossmap = Coding([(10, 20)], (15, 20), True) @@ -788,3 +810,78 @@ def test_Coding_protein(): crossmap.protein_to_coordinate, {'position': 1, 'position_in_codon': 1, 'offset': 0, 'region': 'd'} ) + + +def test_Coding_inverted_protein(): + """Protein positions.""" + crossmap = Coding(_exons, _cds, True) + + # Boundary between upstream and 5' UTR + invariant( + crossmap.coordinate_to_protein, + 4, + crossmap.protein_to_coordinate, + {'position': 1, 'position_in_codon': 1, 'offset': 0, 'region': 'd'} + ) + invariant( + crossmap.coordinate_to_protein, + 5, + crossmap.protein_to_coordinate, + {'position': 4, 'position_in_codon': 2, 'offset': 0, 'region': '*'} + ) + + # Boundary between 5' UTR and CDS + invariant( + crossmap.coordinate_to_protein, + 31, + crossmap.protein_to_coordinate, + {'position': 1, 'position_in_codon': 1, 'offset': 0, 'region': '*'}, + ) + invariant( + crossmap.coordinate_to_protein, + 32, + crossmap.protein_to_coordinate, + {'position': 2, 'position_in_codon': 3, 'offset': 0, 'region': ''}, + ) + + # Intron boundary. + invariant( + crossmap.coordinate_to_protein, + 34, + crossmap.protein_to_coordinate, + {'position': 2, 'position_in_codon': 1, 'offset': 0, 'region': ''}, + ) + invariant( + crossmap.coordinate_to_protein, + 35, + crossmap.protein_to_coordinate, + {'position': 2, 'position_in_codon': 1, 'offset': -1, 'region': ''}, + ) + + # Boundary between CDS and 3' UTR. + invariant( + crossmap.coordinate_to_protein, + 42, + crossmap.protein_to_coordinate, + {'position': 1, 'position_in_codon': 1, 'offset': 0, 'region': ''}, + ) + invariant( + crossmap.coordinate_to_protein, + 43, + crossmap.protein_to_coordinate, + {'position': 1, 'position_in_codon': 3, 'offset': 0, 'region': '-'}, + ) + + # Boundary between 3' UTR and downstream + invariant( + crossmap.coordinate_to_protein, + 71, + crossmap.protein_to_coordinate, + {'position': 2, 'position_in_codon': 2, 'offset': 0, 'region': '-'} + ) + invariant( + crossmap.coordinate_to_protein, + 72, + crossmap.protein_to_coordinate, + {'position': 1, 'position_in_codon': 3, 'offset': 0, 'region': 'u'} + ) From 50d50b57798c90396f5792c5ccf5692914d3551d Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 31 Mar 2026 13:37:16 +0200 Subject: [PATCH 121/236] Rename external (input/output) variable to pos_m and add test for a one base intron in coding --- mutalyzer_crossmapper/crossmapper.py | 66 ++++++++++++++++------------ tests/test_crossmapper.py | 22 ++++++++++ 2 files changed, 59 insertions(+), 29 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 7a38392..d10ec14 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -33,7 +33,11 @@ def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> self._noncoding = MultiLocus(locations, inverted) - def coordinate_to_noncoding(self, coordinate: int, degenerate: bool = False) -> dict: + def coordinate_to_noncoding( + self, + coordinate: int, + degenerate: bool = False + ) -> dict[str, int | str]: """Convert a coordinate to a noncoding position (n./r.). :arg int coordinate: Coordinate. @@ -41,17 +45,14 @@ def coordinate_to_noncoding(self, coordinate: int, degenerate: bool = False) -> :returns dict: Noncoding position model. """ multilocus_pos_m = self._noncoding.to_position(coordinate) - noncoding_pos_m = {**multilocus_pos_m, 'position': multilocus_pos_m['position'] + 1} - region = noncoding_pos_m['region'] - if region == '': - return noncoding_pos_m - + pos_m = {**multilocus_pos_m, 'position': multilocus_pos_m['position'] + 1} + region = pos_m['region'] if degenerate: if region == 'u': - noncoding_pos_m['region'] = '-' + pos_m['region'] = '-' elif region == 'd': - noncoding_pos_m['region'] = '*' - return noncoding_pos_m + pos_m['region'] = '*' + return pos_m def noncoding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: """Convert a noncoding position (n./r.) to a coordinate. @@ -61,9 +62,9 @@ def noncoding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: :returns int: Coordinate. """ multilocus_pos_m = {**pos_m, 'position': pos_m['position'] - 1} - if multilocus_pos_m['region'] == '-': + if pos_m['region'] == '-': multilocus_pos_m['region'] = 'u' - elif multilocus_pos_m['region'] == '*': + elif pos_m['region'] == '*': multilocus_pos_m['region'] = 'd' return self._noncoding.to_coordinate(multilocus_pos_m) @@ -83,17 +84,25 @@ def __init__( """ NonCoding.__init__(self, locations, inverted) - b0 = self._noncoding.to_position(cds[0]) - b1 = self._noncoding.to_position(cds[1] - 1) - e0 = self._noncoding.to_position(locations[0][0]) - e1 = self._noncoding.to_position(locations[-1][1] - 1) + cds_start = self._noncoding.to_position(cds[0]) + cds_end = self._noncoding.to_position(cds[1] - 1) + exons_start = self._noncoding.to_position(locations[0][0]) + exons_end = self._noncoding.to_position(locations[-1][1] - 1) if self._inverted: - self._coding = (b1['position'] + b1['offset'], b0['position'] + b0['offset'] + 1) - self._exons = (e1['position'], e0['position'] + 1) + self._coding = ( + cds_end['position'] + cds_end['offset'], + cds_start['position'] + cds_start['offset'] + 1 + ) + # Used in degenerate option + self._exons_len = exons_start['position'] + 1 else: - self._coding = (b0['position'] + b0['offset'], b1['position'] + b1['offset'] + 1) - self._exons = (e0['position'], e1['position'] + 1) + self._coding = ( + cds_start['position'] + cds_start['offset'], + cds_end['position'] + cds_end['offset'] + 1 + ) + # Used in degenerate option + self._exons_len = exons_end['position'] + 1 def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: """Convert a coordinate to a coding position (c./r.). @@ -145,7 +154,7 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dic location = pos_m['position'] if region == 'u': if self._inverted: - degenerate_pos_m['position'] = location + self._exons[1] - self._coding[1] + degenerate_pos_m['position'] = location + self._exons_len - self._coding[1] else: degenerate_pos_m['position'] = location + self._coding[0] degenerate_pos_m['region'] = '-' @@ -153,7 +162,7 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dic if self._inverted: degenerate_pos_m['position'] = location + self._coding[0] else: - degenerate_pos_m['position'] = location + self._exons[1] - self._coding[1] + degenerate_pos_m['position'] = location + self._exons_len - self._coding[1] degenerate_pos_m['region'] = '*' return degenerate_pos_m @@ -179,7 +188,6 @@ def _coding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: multilocus_pos_m['position'] = self._coding[0] - location else: multilocus_pos_m['position'] = self._coding[1] + location - 1 - return self._noncoding.to_coordinate(multilocus_pos_m) def coding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: @@ -199,20 +207,20 @@ def coordinate_to_protein(self, coordinate: int) -> dict[str, int | str]: :returns dict: Protein position model(p.). """ - pos = self.coordinate_to_coding(coordinate) + pos_m = self.coordinate_to_coding(coordinate) - location = pos['position'] - if pos['region'] in ('-', 'u'): + location = pos_m['position'] + if pos_m['region'] in ('-', 'u'): return { 'position': abs(-location // 3), 'position_in_codon': -location % 3 + 1, - 'region': pos['region'], - 'offset': pos['offset']} + 'region': pos_m['region'], + 'offset': pos_m['offset']} return { 'position': (location + 2) // 3, 'position_in_codon': (location + 2) % 3 + 1, - 'region': pos['region'], - 'offset': pos['offset']} + 'region': pos_m['region'], + 'offset': pos_m['offset']} def protein_to_coordinate(self, pos_m: dict[str, int | str]) -> int: """Convert a protein position (p.) to a coordinate. diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 87b5ea7..098508b 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -373,6 +373,28 @@ def test_Coding_no_intron_inverted(): ) +def test_Coding_one_base_intron(): + crossmap = Coding([(10, 19), (20, 30)], (15, 25)) + + invariant( + crossmap.coordinate_to_coding, + 19, + crossmap.coding_to_coordinate, + {'position': 4, 'offset': 1, 'region': ''}, + ) + + +def test_Coding_one_base_intron_inverted(): + crossmap = Coding([(10, 19), (20, 30)], (15, 25), True) + + invariant( + crossmap.coordinate_to_coding, + 19, + crossmap.coding_to_coordinate, + {'position': 5, 'offset': 1, 'region': ''}, + ) + + def test_Coding_no_utr5_inverted(): """A 5' UTR may be missing.""" crossmap = Coding([(10, 20)], (15, 20), True) From 73e2965be5899da0e1f02d0e18df49e42e9645cc Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 31 Mar 2026 14:29:57 +0200 Subject: [PATCH 122/236] Fix typo and font --- docs/library.rst | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 6b358f9..7720088 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -22,7 +22,7 @@ They are represented as 1-key dictionaries. Below is an example of ``g.1`` in HG Where: -- **position**: an integer representing a nucleotide position (>0) +- **position**: an integer representing a nucleotide position (> 0) Genomic Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -68,7 +68,7 @@ as 3-key dictionaries. Below is an example of ``n.14+1`` in HGVS. Where: -- **position**: an integer representing a transcript position (>0) +- **position**: an integer representing a nucleotide position (> 0) - **offset**: an integer indicating the offset relative to the position (negative for upstream, positive for downstream) - **region**: a string describing the region type (empty for positions within a non-coding @@ -86,7 +86,7 @@ NonCoding Position Conversion Now the functions ``coordinate_to_noncoding()`` and ``noncoding_to_coordinate()`` can be used. -In our example, the HGVS position ``g.36`` (coordinate `35`) is equivalent to +In our example, the HGVS position ``g.36`` (coordinate *35*) is equivalent to position ``n.14+1``. We can convert between these two as follows. .. code:: python @@ -96,8 +96,8 @@ position ``n.14+1``. We can convert between these two as follows. >>> crossmap.noncoding_to_coordinate({'position': 14, 'offset': 1, 'region': ''}) 35 -When the coordinate is upstream or downstream of the transcript, we use ``u`` to -present upstream and ``d`` to present downstream. +When the coordinate is upstream or downstream of the transcript, we use ``u`` to +denote upstream and ``d`` to denote downstream. .. code:: python @@ -124,7 +124,7 @@ instead. For transcripts that reside on the reverse complement strand, the ``inverted`` parameter should be set to ``True``. In our example, HGVS position ``g.36`` -(coordinate `35`) is now equivalent to position ``n.9-1``. +(coordinate *35*) is now equivalent to position ``n.9-1``. .. code:: python @@ -210,7 +210,7 @@ represented as 3-key dictionaries. Here is an example of ``c.*1+3``. Where: -- **position**: an integer representing a transcript position (>0) +- **position**: an integer representing a transcript position (> 0) - **offset**: an integer indicating the offset relative to the position (negative for upstream, positive for downstream) - **region**: a string describing the region type (empty for positions within coding DNA sequence, @@ -230,7 +230,7 @@ On top of the functionality provided by the ``NonCoding`` class, the functions ``coordinate_to_coding()`` and ``coding_to_coordinate()`` can be used. These functions use a 3-key dictionary to represent a coding position. -In our example, the HGVS position ``g.32`` (coordinate `31`) is equivalent to +In our example, the HGVS position ``g.32`` (coordinate *31*) is equivalent to position ``c.-1``. We can convert between these two as follows. .. code:: python @@ -335,7 +335,7 @@ Protein Additionally, the functions ``coordinate_to_protein()`` and ``protein_to_coordinate()`` can be used. These functions use a 4-key dictionary -to represent a protein position. Here is one example of three posibilities +to represent a protein position. Here is one example of three possibilities for ``p.1`` in HGVS. .. code-block:: python @@ -349,13 +349,13 @@ for ``p.1`` in HGVS. Where: -- **position**: an integer representing an amino acid position (>0) -- **position_in_codon**: an integer indicating the nucleotide index within the codon (1, 2, or 3) +- **position**: an integer representing an amino acid position (> 0) +- **position_in_codon**: an integer indexing the position in a codon (1, 2, or 3) - **offset**: an integer indicating offset relative to the nucleotide specified by `position_in_codon` in the codon -- **region**: a string describing the region type (empty for vaid amino acid positions) +- **region**: a string describing the region type (empty for valid amino acid positions) -In our example the HGVS position ``g.42`` (coordinate `41`) corresponds with -position ``p.2``. We can convert between these to as follows. +In our example, the HGVS position ``g.42`` (coordinate *41*) corresponds with +position ``p.2``. We can convert between these two as follows. .. code:: python From 3ec8ab476a021a2a8369067c4b8791b5ba628f00 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 1 Apr 2026 11:37:08 +0200 Subject: [PATCH 123/236] Update degenerate for noncoding and tests --- mutalyzer_crossmapper/crossmapper.py | 16 ++++++++++++++-- tests/test_crossmapper.py | 21 ++++++++++----------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index d10ec14..2e948bd 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -32,6 +32,10 @@ def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> self._inverted = inverted self._noncoding = MultiLocus(locations, inverted) + if self._inverted: + self._exons_len = self._noncoding.to_position(locations[0][0])['position'] + 1 + else: + self._exons_len = self._noncoding.to_position(locations[-1][1] - 1)['position'] + 1 def coordinate_to_noncoding( self, @@ -50,8 +54,12 @@ def coordinate_to_noncoding( if degenerate: if region == 'u': pos_m['region'] = '-' + pos_m['offset'] = -pos_m['position'] + pos_m['position'] = 1 elif region == 'd': pos_m['region'] = '*' + pos_m['offset'] = pos_m['position'] + pos_m['position'] = self._exons_len return pos_m def noncoding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: @@ -62,10 +70,14 @@ def noncoding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: :returns int: Coordinate. """ multilocus_pos_m = {**pos_m, 'position': pos_m['position'] - 1} - if pos_m['region'] == '-': + if pos_m['region'] == '-': # degenerate results multilocus_pos_m['region'] = 'u' - elif pos_m['region'] == '*': + multilocus_pos_m['position'] = abs(pos_m['offset']) -1 + multilocus_pos_m['offset'] = 0 + if pos_m['region'] == '*': # degenerate results multilocus_pos_m['region'] = 'd' + multilocus_pos_m['position'] = abs(pos_m['offset']) -1 + multilocus_pos_m['offset'] = 0 return self._noncoding.to_coordinate(multilocus_pos_m) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 098508b..bc33282 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -107,8 +107,7 @@ def test_NonCoding_degenerate(): [ {'position': 1, 'offset': -1, 'region': ''}, {'position': 1, 'offset': 0, 'region': 'u'}, - {'position': 1, 'offset': 0, 'region': '-'}, - {'position': 2, 'offset': 1, 'region': '-'}, + {'position': 1, 'offset': -1, 'region': '-'}, ], ) @@ -121,7 +120,7 @@ def test_NonCoding_degenerate(): {'position': 22, 'offset': 1, 'region': ''}, {'position': 23, 'offset': 0, 'region': ''}, {'position': 24, 'offset': -1, 'region': ''}, - {'position': 1, 'offset': 0, 'region': '*'}, + {'position': 22, 'offset': 1, 'region': '*'}, # standard degenerate result ], ) @@ -137,7 +136,7 @@ def test_NonCoding_inverted_degenerate(): [ {'position': 1, 'offset': -1, 'region': ''}, {'position': 1, 'offset': 0, 'region': 'u'}, - {'position': 1, 'offset': 0, 'region': '-'}, + {'position': 1, 'offset': -1, 'region': '-'}, ], ) @@ -147,7 +146,7 @@ def test_NonCoding_inverted_degenerate(): 4, [ {'position': 1, 'offset': 0, 'region': 'd'}, - {'position': 1, 'offset': 0, 'region': '*'}, + {'position': 22, 'offset': 1, 'region': '*'}, {'position': 23, 'offset': 0, 'region': ''}, {'position': 22, 'offset': 1, 'region': ''}, ], @@ -159,13 +158,13 @@ def test_NonCoding_degenerate_return(): assert crossmap.coordinate_to_noncoding(4, True) == { 'position': 1, - 'offset': 0, + 'offset': -1, 'region': '-', } assert crossmap.coordinate_to_noncoding(72, True) == { - 'position': 1, - 'offset': 0, + 'position': 22, + 'offset': 1, 'region': '*', } @@ -175,13 +174,13 @@ def test_NonCoding_inverted_degenerate_return(): assert crossmap.coordinate_to_noncoding(72, True) == { 'position': 1, - 'offset': 0, + 'offset': -1, 'region': '-', } assert crossmap.coordinate_to_noncoding(4, True) == { - 'position': 1, - 'offset': 0, + 'position': 22, + 'offset': 1, 'region': '*', } From ba091d536a4f323db490174d4e6421aee75795b1 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 1 Apr 2026 12:04:53 +0200 Subject: [PATCH 124/236] Update degenerate for noncoding example in documentation --- docs/library.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 7720088..64aa788 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -113,14 +113,14 @@ denote upstream and ``d`` to denote downstream. The ``coordinate_to_noncoding()`` function accepts an optional ``degenerate`` argument. When set to ``True``, positions outside of the transcript are no longer described using the ``u`` or ``d`` notation, ``-`` and ``*`` are used -instead. +instead. The values in ``position`` and ``offset`` will change accordingly. .. code:: python >>> crossmap.coordinate_to_noncoding(2) {'position': 3, 'offset': 0, 'region': 'u'} >>> crossmap.coordinate_to_noncoding(2, True) - {'position': 3, 'offset': 0, 'region': '-'} + {'position': 1, 'offset': -3, 'region': '-'} For transcripts that reside on the reverse complement strand, the ``inverted`` parameter should be set to ``True``. In our example, HGVS position ``g.36`` From 39d1d6b5fe5a09e112f61b310da6cb6a751327b4 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 1 Apr 2026 15:02:02 +0200 Subject: [PATCH 125/236] Discard unneccessary functions for coding --- mutalyzer_crossmapper/crossmapper.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 2e948bd..117a62e 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -178,7 +178,7 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dic degenerate_pos_m['region'] = '*' return degenerate_pos_m - def _coding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: + def coding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: """Convert a coding position (c./r.) to a coordinate. :arg dict pos_m: Coding position model (c./r.). @@ -202,16 +202,6 @@ def _coding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: multilocus_pos_m['position'] = self._coding[1] + location - 1 return self._noncoding.to_coordinate(multilocus_pos_m) - def coding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: - """Convert a coding position (c./r.) to a coordinate. - - :arg dict pos_m: Coding position model (c./r.). - - :returns int: Coordinate. - """ - - return self._coding_to_coordinate(pos_m) - def coordinate_to_protein(self, coordinate: int) -> dict[str, int | str]: """Convert a coordinate to a protein position (p.). From 915e8453a90bdf470b60d2c11dee11462598e84c Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 1 Apr 2026 15:15:43 +0200 Subject: [PATCH 126/236] Cleanup --- mutalyzer_crossmapper/crossmapper.py | 8 ++++---- tests/test_crossmapper.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 117a62e..0687b2b 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -70,13 +70,13 @@ def noncoding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: :returns int: Coordinate. """ multilocus_pos_m = {**pos_m, 'position': pos_m['position'] - 1} - if pos_m['region'] == '-': # degenerate results + if pos_m['region'] == '-': # degenerate results multilocus_pos_m['region'] = 'u' - multilocus_pos_m['position'] = abs(pos_m['offset']) -1 + multilocus_pos_m['position'] = abs(pos_m['offset']) - 1 multilocus_pos_m['offset'] = 0 - if pos_m['region'] == '*': # degenerate results + if pos_m['region'] == '*': # degenerate results multilocus_pos_m['region'] = 'd' - multilocus_pos_m['position'] = abs(pos_m['offset']) -1 + multilocus_pos_m['position'] = abs(pos_m['offset']) - 1 multilocus_pos_m['offset'] = 0 return self._noncoding.to_coordinate(multilocus_pos_m) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index bc33282..a186937 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -120,7 +120,7 @@ def test_NonCoding_degenerate(): {'position': 22, 'offset': 1, 'region': ''}, {'position': 23, 'offset': 0, 'region': ''}, {'position': 24, 'offset': -1, 'region': ''}, - {'position': 22, 'offset': 1, 'region': '*'}, # standard degenerate result + {'position': 22, 'offset': 1, 'region': '*'}, # standard degenerate result ], ) From 69eb98589a7851f6f2dbec3a480eca92ad25e895 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 1 Apr 2026 16:04:40 +0200 Subject: [PATCH 127/236] Fix error for inverted coding --- mutalyzer_crossmapper/crossmapper.py | 10 ++-------- tests/test_crossmapper.py | 30 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 0687b2b..88cba2e 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -165,16 +165,10 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dic degenerate_pos_m = {**pos_m, 'offset': pos_m['offset']} location = pos_m['position'] if region == 'u': - if self._inverted: - degenerate_pos_m['position'] = location + self._exons_len - self._coding[1] - else: - degenerate_pos_m['position'] = location + self._coding[0] + degenerate_pos_m['position'] = location + self._coding[0] degenerate_pos_m['region'] = '-' if region == 'd': - if self._inverted: - degenerate_pos_m['position'] = location + self._coding[0] - else: - degenerate_pos_m['position'] = location + self._exons_len - self._coding[1] + degenerate_pos_m['position'] = location + self._exons_len - self._coding[1] degenerate_pos_m['region'] = '*' return degenerate_pos_m diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index a186937..dcdb718 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -639,6 +639,11 @@ def test_Coding_inverted_degenerate_return(): 'offset': 0, 'region': '-', } + assert crossmap.coordinate_to_coding(25, True) == { + 'position': 7, + 'offset': 0, + 'region': '-', + } assert crossmap.coordinate_to_coding(9, True) == { 'position': 2, 'offset': 0, @@ -646,6 +651,31 @@ def test_Coding_inverted_degenerate_return(): } +def test_Coding_two_exons_inverted_degenerate_return(): + """Degenerate upstream and downstream positions may be returned.""" + crossmap = Coding([(10, 20), (30, 40)], (18, 37), True) + + assert crossmap.coordinate_to_coding(5, True) == { + 'position': 13, + 'offset': 0, + 'region': '*', + } + assert crossmap.coordinate_to_coding(25, True) == { + 'position': 7, + 'offset': 5, + 'region': '', + } + assert crossmap.coordinate_to_coding(35, True) == { + 'position': 2, + 'offset': 0, + 'region': '', + } + assert crossmap.coordinate_to_coding(38, True) == { + 'position': 2, + 'offset': 0, + 'region': '-', + } + def test_Coding_degenerate_no_return(): """Degenerate internal positions do not exist.""" crossmap = Coding([(10, 20), (30, 40)], (10, 40)) From c08b52b765ca99b6b947869243a12ff029763470 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 7 May 2026 14:58:17 +0200 Subject: [PATCH 128/236] Treat upstream and downstream as introns. --- mutalyzer_crossmapper/crossmapper.py | 32 ++++------------------------ mutalyzer_crossmapper/multi_locus.py | 15 ++++--------- 2 files changed, 8 insertions(+), 39 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 88cba2e..44a28fc 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -32,10 +32,6 @@ def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> self._inverted = inverted self._noncoding = MultiLocus(locations, inverted) - if self._inverted: - self._exons_len = self._noncoding.to_position(locations[0][0])['position'] + 1 - else: - self._exons_len = self._noncoding.to_position(locations[-1][1] - 1)['position'] + 1 def coordinate_to_noncoding( self, @@ -54,12 +50,8 @@ def coordinate_to_noncoding( if degenerate: if region == 'u': pos_m['region'] = '-' - pos_m['offset'] = -pos_m['position'] - pos_m['position'] = 1 elif region == 'd': pos_m['region'] = '*' - pos_m['offset'] = pos_m['position'] - pos_m['position'] = self._exons_len return pos_m def noncoding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: @@ -70,14 +62,6 @@ def noncoding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: :returns int: Coordinate. """ multilocus_pos_m = {**pos_m, 'position': pos_m['position'] - 1} - if pos_m['region'] == '-': # degenerate results - multilocus_pos_m['region'] = 'u' - multilocus_pos_m['position'] = abs(pos_m['offset']) - 1 - multilocus_pos_m['offset'] = 0 - if pos_m['region'] == '*': # degenerate results - multilocus_pos_m['region'] = 'd' - multilocus_pos_m['position'] = abs(pos_m['offset']) - 1 - multilocus_pos_m['offset'] = 0 return self._noncoding.to_coordinate(multilocus_pos_m) @@ -98,23 +82,17 @@ def __init__( cds_start = self._noncoding.to_position(cds[0]) cds_end = self._noncoding.to_position(cds[1] - 1) - exons_start = self._noncoding.to_position(locations[0][0]) - exons_end = self._noncoding.to_position(locations[-1][1] - 1) if self._inverted: self._coding = ( cds_end['position'] + cds_end['offset'], cds_start['position'] + cds_start['offset'] + 1 ) - # Used in degenerate option - self._exons_len = exons_start['position'] + 1 else: self._coding = ( cds_start['position'] + cds_start['offset'], cds_end['position'] + cds_end['offset'] + 1 ) - # Used in degenerate option - self._exons_len = exons_end['position'] + 1 def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: """Convert a coordinate to a coding position (c./r.). @@ -124,9 +102,10 @@ def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: :returns dict: Coding position model (c./r.). """ multilocus_pos_m = self._noncoding.to_position(coordinate) - - if multilocus_pos_m['region'] in ('u', 'd'): - return {**multilocus_pos_m, 'position': multilocus_pos_m['position'] + 1} + if multilocus_pos_m['region'] == 'u': + return {**multilocus_pos_m, 'position': self._coding[0]} + if multilocus_pos_m['region'] == 'd': + return {**multilocus_pos_m, 'position': multilocus_pos_m['position'] - self._coding[1] +1} location = multilocus_pos_m['position'] offset = multilocus_pos_m['offset'] @@ -163,12 +142,9 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dic return pos_m degenerate_pos_m = {**pos_m, 'offset': pos_m['offset']} - location = pos_m['position'] if region == 'u': - degenerate_pos_m['position'] = location + self._coding[0] degenerate_pos_m['region'] = '-' if region == 'd': - degenerate_pos_m['position'] = location + self._exons_len - self._coding[1] degenerate_pos_m['region'] = '*' return degenerate_pos_m diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index d2c5662..165f722 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -60,13 +60,6 @@ def to_position(self, coordinate: int) -> dict[str, int | str]: outside = self._orientation * self.outside(coordinate) region = 'u' if outside < 0 else 'd' if outside > 0 else '' locus_pos_m = self._loci[index].to_position(coordinate) - - if outside: - return { - 'position': abs(locus_pos_m['offset']) - 1, - 'offset': 0, - 'region': region - } return { 'position': locus_pos_m['position'] + self._offsets[self._direction(index)], 'offset': locus_pos_m['offset'], @@ -84,12 +77,12 @@ def to_coordinate(self, pos_m: dict[str, int | str]) -> int: if region == 'u': if self._inverted: - return self._locations[-1][1] + abs(pos_m['position']) - pos_m['offset'] - return self._locations[0][0] - abs(pos_m['position']) + pos_m['offset'] - 1 + return self._locations[-1][1] - pos_m['offset'] - 1 + return self._locations[0][0] + pos_m['offset'] if region == 'd': if self._inverted: - return self._locations[0][0] - abs(pos_m['position']) - pos_m['offset'] - 1 - return self._locations[-1][1] + abs(pos_m['position']) + pos_m['offset'] + return self._locations[0][0] - pos_m['offset'] + return self._locations[-1][1] + pos_m['offset'] - 1 index = min( len(self._offsets), From 87818ca4546cff06371445b65fc72d820b5defc8 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 8 May 2026 12:47:13 +0200 Subject: [PATCH 129/236] Local change before swith branch --- .gitignore | 4 ++ mutalyzer_crossmapper/crossmapper.py | 24 ++++---- tests/test_crossmapper.py | 89 +++++++--------------------- tests/test_multi_locus.py | 16 ++--- 4 files changed, 45 insertions(+), 88 deletions(-) diff --git a/.gitignore b/.gitignore index 381420f..1075310 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ docs/_build/ mutalyzer_crossmapper.egg-info/ mutalyzer_crossmapper/__pycache__/ tests/__pycache__/ +<<<<<<< Updated upstream +======= +tmp/ +>>>>>>> Stashed changes diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 44a28fc..77a5611 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -36,7 +36,6 @@ def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> def coordinate_to_noncoding( self, coordinate: int, - degenerate: bool = False ) -> dict[str, int | str]: """Convert a coordinate to a noncoding position (n./r.). @@ -45,14 +44,7 @@ def coordinate_to_noncoding( :returns dict: Noncoding position model. """ multilocus_pos_m = self._noncoding.to_position(coordinate) - pos_m = {**multilocus_pos_m, 'position': multilocus_pos_m['position'] + 1} - region = pos_m['region'] - if degenerate: - if region == 'u': - pos_m['region'] = '-' - elif region == 'd': - pos_m['region'] = '*' - return pos_m + return {**multilocus_pos_m, 'position': multilocus_pos_m['position'] + 1} def noncoding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: """Convert a noncoding position (n./r.) to a coordinate. @@ -82,6 +74,8 @@ def __init__( cds_start = self._noncoding.to_position(cds[0]) cds_end = self._noncoding.to_position(cds[1] - 1) + print("csd start", cds_start) + print("cds end:", cds_end) if self._inverted: self._coding = ( @@ -102,10 +96,13 @@ def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: :returns dict: Coding position model (c./r.). """ multilocus_pos_m = self._noncoding.to_position(coordinate) + print(multilocus_pos_m) if multilocus_pos_m['region'] == 'u': + # print(self._coding) + return {**multilocus_pos_m, 'position': self._coding[0]} if multilocus_pos_m['region'] == 'd': - return {**multilocus_pos_m, 'position': multilocus_pos_m['position'] - self._coding[1] +1} + return {**multilocus_pos_m, 'position': multilocus_pos_m['position'] - self._coding[1] + 1} location = multilocus_pos_m['position'] offset = multilocus_pos_m['offset'] @@ -138,14 +135,17 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dic pos_m = self._coordinate_to_coding(coordinate) region = pos_m['region'] - if not degenerate or region == '': + if not degenerate: return pos_m - degenerate_pos_m = {**pos_m, 'offset': pos_m['offset']} if region == 'u': degenerate_pos_m['region'] = '-' + degenerate_pos_m['position'] = degenerate_pos_m['position'] + abs(degenerate_pos_m['offset']) + degenerate_pos_m['offset'] = 0 if region == 'd': degenerate_pos_m['region'] = '*' + degenerate_pos_m['position'] = degenerate_pos_m['position'] + abs(degenerate_pos_m['offset']) + degenerate_pos_m['offset'] = 0 return degenerate_pos_m def coding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index dcdb718..c7cc38f 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -33,13 +33,13 @@ def test_NonCoding(): crossmap.coordinate_to_noncoding, 3, crossmap.noncoding_to_coordinate, - {'position': 2, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': -2, 'region': 'u'}, ) invariant( crossmap.coordinate_to_noncoding, 4, crossmap.noncoding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': -1, 'region': 'u'}, ) invariant( crossmap.coordinate_to_noncoding, @@ -59,7 +59,7 @@ def test_NonCoding(): crossmap.coordinate_to_noncoding, 72, crossmap.noncoding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 22, 'offset': 1, 'region': 'd'}, ) @@ -72,7 +72,7 @@ def test_NonCoding_inverted(): crossmap.coordinate_to_noncoding, 72, crossmap.noncoding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': -1, 'region': 'u'}, ) invariant( crossmap.coordinate_to_noncoding, @@ -92,7 +92,7 @@ def test_NonCoding_inverted(): crossmap.coordinate_to_noncoding, 4, crossmap.noncoding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 22, 'offset': 1, 'region': 'd'}, ) @@ -106,8 +106,7 @@ def test_NonCoding_degenerate(): 4, [ {'position': 1, 'offset': -1, 'region': ''}, - {'position': 1, 'offset': 0, 'region': 'u'}, - {'position': 1, 'offset': -1, 'region': '-'}, + {'position': 1, 'offset': -1, 'region': 'u'}, ], ) @@ -116,7 +115,7 @@ def test_NonCoding_degenerate(): crossmap.noncoding_to_coordinate, 72, [ - {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 22, 'offset': 1, 'region': 'd'}, {'position': 22, 'offset': 1, 'region': ''}, {'position': 23, 'offset': 0, 'region': ''}, {'position': 24, 'offset': -1, 'region': ''}, @@ -135,7 +134,7 @@ def test_NonCoding_inverted_degenerate(): 72, [ {'position': 1, 'offset': -1, 'region': ''}, - {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': -1, 'region': 'u'}, {'position': 1, 'offset': -1, 'region': '-'}, ], ) @@ -145,7 +144,7 @@ def test_NonCoding_inverted_degenerate(): crossmap.noncoding_to_coordinate, 4, [ - {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 22, 'offset': 1, 'region': 'd'}, {'position': 22, 'offset': 1, 'region': '*'}, {'position': 23, 'offset': 0, 'region': ''}, {'position': 22, 'offset': 1, 'region': ''}, @@ -153,52 +152,6 @@ def test_NonCoding_inverted_degenerate(): ) -def test_NonCoding_degenerate_return(): - crossmap = NonCoding(_exons) - - assert crossmap.coordinate_to_noncoding(4, True) == { - 'position': 1, - 'offset': -1, - 'region': '-', - } - - assert crossmap.coordinate_to_noncoding(72, True) == { - 'position': 22, - 'offset': 1, - 'region': '*', - } - - -def test_NonCoding_inverted_degenerate_return(): - crossmap = NonCoding(_exons, True) - - assert crossmap.coordinate_to_noncoding(72, True) == { - 'position': 1, - 'offset': -1, - 'region': '-', - } - - assert crossmap.coordinate_to_noncoding(4, True) == { - 'position': 22, - 'offset': 1, - 'region': '*', - } - - -def test_NonCoding_degenerate_no_return(): - """Degenerate internal positions do not exist.""" - crossmap = NonCoding(_exons) - - assert crossmap.coordinate_to_noncoding(25) == crossmap.coordinate_to_noncoding(25, True) - - -def test_NonCoding_inverted_degenerate_no_return(): - """Degenerate internal positions do not exist.""" - crossmap = NonCoding(_exons, True) - - assert crossmap.coordinate_to_noncoding(25) == crossmap.coordinate_to_noncoding(25, True) - - def test_Coding(): """Forward oriented coding transcript.""" crossmap = Coding(_exons, _cds) @@ -340,7 +293,7 @@ def test_Coding_no_utr5(): crossmap.coordinate_to_coding, 9, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': -1, 'region': 'u'}, ) invariant( crossmap.coordinate_to_coding, @@ -403,7 +356,7 @@ def test_Coding_no_utr5_inverted(): crossmap.coordinate_to_coding, 20, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': -1, 'region': 'u'}, ) invariant( crossmap.coordinate_to_coding, @@ -428,7 +381,7 @@ def test_Coding_no_utr3(): crossmap.coordinate_to_coding, 20, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 5, 'offset': 1, 'region': 'd'}, ) @@ -447,7 +400,7 @@ def test_Coding_no_utr3_inverted(): crossmap.coordinate_to_coding, 9, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 5, 'offset': 1, 'region': 'd'}, ) @@ -460,7 +413,7 @@ def test_Coding_small_utr5(): crossmap.coordinate_to_coding, 9, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': -1, 'region': 'u'}, ) invariant( crossmap.coordinate_to_coding, @@ -485,7 +438,7 @@ def test_Coding_small_utr5_inverted(): crossmap.coordinate_to_coding, 20, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': -1, 'region': 'u'}, ) invariant( crossmap.coordinate_to_coding, @@ -522,7 +475,7 @@ def test_Coding_small_utr3(): crossmap.coordinate_to_coding, 20, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 1, 'offset': 1, 'region': 'd'}, ) @@ -547,7 +500,7 @@ def test_Coding_small_utr3_inverted(): crossmap.coordinate_to_coding, 9, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 1, 'offset': 1, 'region': 'd'}, ) @@ -560,7 +513,7 @@ def test_Coding_degenerate(): crossmap.coding_to_coordinate, 9, [ - {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': -1, 'region': 'u'}, {'position': 2, 'offset': 0, 'region': '-'}, {'position': 1, 'offset': -2, 'region': ''}, {'position': 1, 'offset': -10, 'region': '*'}, @@ -573,7 +526,7 @@ def test_Coding_degenerate(): crossmap.coding_to_coordinate, 20, [ - {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 9, 'offset': 1, 'region': 'd'}, {'position': 2, 'offset': 0, 'region': '*'}, {'position': 8, 'offset': 2, 'region': ''}, {'position': 1, 'offset': 10, 'region': '-'}, @@ -591,7 +544,7 @@ def test_Coding_inverted_degenerate(): crossmap.coding_to_coordinate, 20, [ - {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': -1, 'region': 'u'}, {'position': 2, 'offset': 0, 'region': '-'}, {'position': 1, 'offset': -2, 'region': ''}, {'position': 1, 'offset': -10, 'region': '*'}, @@ -698,7 +651,7 @@ def test_Coding_no_utr_degenerate(): crossmap.coding_to_coordinate, 9, [ - {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': -1, 'region': 'u'}, {'position': 1, 'offset': 0, 'region': '-'}, {'position': 1, 'offset': -2, 'region': '*'}, {'position': 1, 'offset': -1, 'region': ''}, diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index 9725ad7..d349722 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -35,7 +35,7 @@ def test_MultiLocus(): multi_locus.to_position, 4, multi_locus.to_coordinate, - {'position': 0, 'offset': 0, 'region': 'u'}, + {'position': 0, 'offset': -1, 'region': 'u'}, ) invariant( @@ -94,7 +94,7 @@ def test_MultiLocus(): multi_locus.to_position, 72, multi_locus.to_coordinate, - {'position': 0, 'offset': 0, 'region': 'd'}, + {'position': 21, 'offset': 1, 'region': 'd'}, ) @@ -107,7 +107,7 @@ def test_MultiLocus_inverted(): multi_locus.to_position, 72, multi_locus.to_coordinate, - {'position': 0, 'offset': 0, 'region': 'u'}, + {'position': 0, 'offset': -1, 'region': 'u'}, ) invariant( multi_locus.to_position, @@ -165,7 +165,7 @@ def test_MultiLocus_inverted(): multi_locus.to_position, 4, multi_locus.to_coordinate, - {'position': 0, 'offset': 0, 'region': 'd'}, + {'position': 21, 'offset': 1, 'region': 'd'}, ) @@ -287,7 +287,7 @@ def test_MultiLocus_degenerate(): [ {'position': 0, 'offset': -1, 'region': ''}, {'position': -1, 'offset': 0, 'region': ''}, - {'position': 0, 'offset': 0, 'region': 'u'}, + {'position': 0, 'offset': -1, 'region': 'u'}, ], ) @@ -297,7 +297,7 @@ def test_MultiLocus_degenerate(): [ {'position': 21, 'offset': 1, 'region': ''}, {'position': 22, 'offset': 0, 'region': ''}, - {'position': 0, 'offset': 0, 'region': 'd'}, + {'position': 22, 'offset': 1, 'region': 'd'}, ], ) @@ -312,7 +312,7 @@ def test_MultiLocus_inverted_degenerate(): [ {'position': -1, 'offset': 0, 'region': ''}, {'position': 0, 'offset': -1, 'region': ''}, - {'position': 0, 'offset': 0, 'region': 'u'}, + {'position': 0, 'offset': -1, 'region': 'u'}, ], ) @@ -322,6 +322,6 @@ def test_MultiLocus_inverted_degenerate(): [ {'position': 21, 'offset': 1, 'region': ''}, {'position': 22, 'offset': 0, 'region': ''}, - {'position': 0, 'offset': 0, 'region': 'd'}, + {'position': 21, 'offset': 1, 'region': 'd'}, ], ) From 917d50914da367c1a2907c0a605e4c9076639089 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 8 May 2026 16:11:36 +0200 Subject: [PATCH 130/236] Local change before swith branch --- mutalyzer_crossmapper/crossmapper.py | 6 ++++-- tests/test_crossmapper.py | 2 +- tests/test_multi_locus.py | 5 ++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 77a5611..91f06b7 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -44,6 +44,7 @@ def coordinate_to_noncoding( :returns dict: Noncoding position model. """ multilocus_pos_m = self._noncoding.to_position(coordinate) + # print(multilocus_pos_m) return {**multilocus_pos_m, 'position': multilocus_pos_m['position'] + 1} def noncoding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: @@ -87,6 +88,7 @@ def __init__( cds_start['position'] + cds_start['offset'], cds_end['position'] + cds_end['offset'] + 1 ) + print(self._coding) def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: """Convert a coordinate to a coding position (c./r.). @@ -96,9 +98,8 @@ def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: :returns dict: Coding position model (c./r.). """ multilocus_pos_m = self._noncoding.to_position(coordinate) - print(multilocus_pos_m) + # print(coordinate, multilocus_pos_m, ) if multilocus_pos_m['region'] == 'u': - # print(self._coding) return {**multilocus_pos_m, 'position': self._coding[0]} if multilocus_pos_m['region'] == 'd': @@ -133,6 +134,7 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dic :returns dict: Coding position model (c./r.). """ pos_m = self._coordinate_to_coding(coordinate) + # print(coordinate, "after",pos_m) region = pos_m['region'] if not degenerate: diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index c7cc38f..0379047 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -119,7 +119,7 @@ def test_NonCoding_degenerate(): {'position': 22, 'offset': 1, 'region': ''}, {'position': 23, 'offset': 0, 'region': ''}, {'position': 24, 'offset': -1, 'region': ''}, - {'position': 22, 'offset': 1, 'region': '*'}, # standard degenerate result + {'position': 22, 'offset': 1, 'region': '*'}, ], ) diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index d349722..364156c 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -285,9 +285,8 @@ def test_MultiLocus_degenerate(): multi_locus.to_coordinate, 4, [ - {'position': 0, 'offset': -1, 'region': ''}, - {'position': -1, 'offset': 0, 'region': ''}, {'position': 0, 'offset': -1, 'region': 'u'}, + {'position': -1, 'offset': 0, 'region': ''}, ], ) @@ -295,7 +294,7 @@ def test_MultiLocus_degenerate(): multi_locus.to_coordinate, 72, [ - {'position': 21, 'offset': 1, 'region': ''}, + {'position': 21, 'offset': 1, 'region': 'd'}, {'position': 22, 'offset': 0, 'region': ''}, {'position': 22, 'offset': 1, 'region': 'd'}, ], From e205144b3db6cf622701126fcb65aecb697793ec Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 11 May 2026 10:36:33 +0200 Subject: [PATCH 131/236] Implement changes in upstream and downstream, degenerate option; add tests --- mutalyzer_crossmapper/crossmapper.py | 60 +++++++++++++++++++++------- tests/test_crossmapper.py | 31 +++++++------- 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 91f06b7..fb029b7 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -44,7 +44,6 @@ def coordinate_to_noncoding( :returns dict: Noncoding position model. """ multilocus_pos_m = self._noncoding.to_position(coordinate) - # print(multilocus_pos_m) return {**multilocus_pos_m, 'position': multilocus_pos_m['position'] + 1} def noncoding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: @@ -75,20 +74,27 @@ def __init__( cds_start = self._noncoding.to_position(cds[0]) cds_end = self._noncoding.to_position(cds[1] - 1) - print("csd start", cds_start) - print("cds end:", cds_end) + exon_start = self._noncoding.to_position(locations[0][0]) + exon_end = self._noncoding.to_position(locations[-1][1] -1) if self._inverted: self._coding = ( cds_end['position'] + cds_end['offset'], cds_start['position'] + cds_start['offset'] + 1 ) + self._exons = ( + exon_end['position'] + exon_end['offset'], + exon_start['position'] + exon_start['offset'] + 1, + ) else: self._coding = ( cds_start['position'] + cds_start['offset'], cds_end['position'] + cds_end['offset'] + 1 ) - print(self._coding) + self._exons = ( + exon_start['position'] + exon_start['offset'], + exon_end['position'] + exon_end['offset'] + 1, + ) def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: """Convert a coordinate to a coding position (c./r.). @@ -98,15 +104,34 @@ def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: :returns dict: Coding position model (c./r.). """ multilocus_pos_m = self._noncoding.to_position(coordinate) - # print(coordinate, multilocus_pos_m, ) - if multilocus_pos_m['region'] == 'u': - - return {**multilocus_pos_m, 'position': self._coding[0]} - if multilocus_pos_m['region'] == 'd': - return {**multilocus_pos_m, 'position': multilocus_pos_m['position'] - self._coding[1] + 1} location = multilocus_pos_m['position'] offset = multilocus_pos_m['offset'] + region = multilocus_pos_m['region'] + if region=='u': + if self._coding[0] == 0: + return { + 'position': 1, + 'offset': offset, + 'region': 'u' + } + return { + 'position':self._coding[0], + 'offset': offset, + 'region': 'u' + } + if region == 'd': + if self._exons[1] == self._coding[1]: + return { + 'position': self._coding[1] - self._coding[0], + 'offset': offset, + 'region': 'd' + } + return { + 'position': location - self._coding[1] + 1, + 'offset': offset, + 'region': 'd' + } if location < self._coding[0]: return { 'position': self._coding[0] - location, @@ -134,20 +159,25 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dic :returns dict: Coding position model (c./r.). """ pos_m = self._coordinate_to_coding(coordinate) - # print(coordinate, "after",pos_m) region = pos_m['region'] if not degenerate: return pos_m degenerate_pos_m = {**pos_m, 'offset': pos_m['offset']} if region == 'u': - degenerate_pos_m['region'] = '-' - degenerate_pos_m['position'] = degenerate_pos_m['position'] + abs(degenerate_pos_m['offset']) + if self._coding[0] == 0: + degenerate_pos_m['position'] = degenerate_pos_m['position'] + abs(degenerate_pos_m['offset']) - 1 + else: + degenerate_pos_m['position'] = degenerate_pos_m['position'] + abs(degenerate_pos_m['offset']) degenerate_pos_m['offset'] = 0 + degenerate_pos_m['region'] = '-' if region == 'd': - degenerate_pos_m['region'] = '*' - degenerate_pos_m['position'] = degenerate_pos_m['position'] + abs(degenerate_pos_m['offset']) + if self._exons[1] == self._coding[1] : + degenerate_pos_m['position'] = degenerate_pos_m['position'] + abs(degenerate_pos_m['offset']) - 1 + else: + degenerate_pos_m['position'] = degenerate_pos_m['position'] + abs(degenerate_pos_m['offset']) degenerate_pos_m['offset'] = 0 + degenerate_pos_m['region'] = '*' return degenerate_pos_m def coding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 0379047..21c5bbb 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -548,7 +548,7 @@ def test_Coding_inverted_degenerate(): {'position': 2, 'offset': 0, 'region': '-'}, {'position': 1, 'offset': -2, 'region': ''}, {'position': 1, 'offset': -10, 'region': '*'}, - {'position': 1, 'offset': -11, 'region': 'd'}, + {'position': 1, 'offset': -10, 'region': 'd'}, {'position': 2, 'offset': -3, 'region': ''}, ], ) @@ -556,13 +556,11 @@ def test_Coding_inverted_degenerate(): crossmap.coding_to_coordinate, 9, [ - {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 2, 'offset': 1, 'region': 'd'}, {'position': 2, 'offset': 0, 'region': '*'}, {'position': 8, 'offset': 2, 'region': ''}, {'position': 1, 'offset': 10, 'region': '-'}, - {'position': 1, 'offset': 11, 'region': 'u'}, - {'position': 2, 'offset': 12, 'region': 'u'}, - + {'position': 1, 'offset': 10, 'region': 'u'}, ], ) @@ -655,18 +653,17 @@ def test_Coding_no_utr_degenerate(): {'position': 1, 'offset': 0, 'region': '-'}, {'position': 1, 'offset': -2, 'region': '*'}, {'position': 1, 'offset': -1, 'region': ''}, - {'position': 1, 'offset': -2, 'region': 'd'}, + {'position': 1, 'offset': -1, 'region': 'd'}, ], ) degenerate_equal( crossmap.coding_to_coordinate, 11, [ - {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 1, 'offset': 1, 'region': 'd'}, {'position': 1, 'offset': 0, 'region': '*'}, {'position': 1, 'offset': 2, 'region': '-'}, {'position': 1, 'offset': 1, 'region': ''}, - {'position': 1, 'offset': 2, 'region': 'u'}, ], ) @@ -675,26 +672,28 @@ def test_Coding_inverted_no_utr_degenerate(): """UTRs may be missing.""" crossmap = Coding([(10, 11)], (10, 11), True) + degenerate_equal( crossmap.coding_to_coordinate, 11, [ - {'position': 1, 'offset': 0, 'region': 'u'}, + {'position': 1, 'offset': -1, 'region': 'u'}, {'position': 1, 'offset': 0, 'region': '-'}, {'position': 1, 'offset': -2, 'region': '*'}, {'position': 1, 'offset': -1, 'region': ''}, - {'position': 1, 'offset': -2, 'region': 'd'}, + {'position': 1, 'offset': -1, 'region': 'd'}, ], ) + print(crossmap.coding_to_coordinate({'position': 1, 'offset': 1, 'region': 'u'})) degenerate_equal( crossmap.coding_to_coordinate, 9, [ - {'position': 1, 'offset': 0, 'region': 'd'}, + {'position': 1, 'offset': 1, 'region': 'd'}, {'position': 1, 'offset': 0, 'region': '*'}, {'position': 1, 'offset': 2, 'region': '-'}, {'position': 1, 'offset': 1, 'region': ''}, - {'position': 1, 'offset': 2, 'region': 'u'}, + {'position': 1, 'offset': 1, 'region': 'u'}, ], ) @@ -750,7 +749,7 @@ def test_Coding_protein(): crossmap.coordinate_to_protein, 4, crossmap.protein_to_coordinate, - {'position': 1, 'position_in_codon': 3, 'offset': 0, 'region': 'u'} + {'position': 4, 'position_in_codon': 2, 'offset': -1, 'region': 'u'} ) invariant( crossmap.coordinate_to_protein, @@ -812,7 +811,7 @@ def test_Coding_protein(): crossmap.coordinate_to_protein, 72, crossmap.protein_to_coordinate, - {'position': 1, 'position_in_codon': 1, 'offset': 0, 'region': 'd'} + {'position': 2, 'position_in_codon': 2, 'offset': 1, 'region': 'd'} ) @@ -825,7 +824,7 @@ def test_Coding_inverted_protein(): crossmap.coordinate_to_protein, 4, crossmap.protein_to_coordinate, - {'position': 1, 'position_in_codon': 1, 'offset': 0, 'region': 'd'} + {'position': 4, 'position_in_codon': 2, 'offset': 1, 'region': 'd'} ) invariant( crossmap.coordinate_to_protein, @@ -887,5 +886,5 @@ def test_Coding_inverted_protein(): crossmap.coordinate_to_protein, 72, crossmap.protein_to_coordinate, - {'position': 1, 'position_in_codon': 3, 'offset': 0, 'region': 'u'} + {'position': 2, 'position_in_codon': 2, 'offset': -1, 'region': 'u'} ) From 07c537bcc1c205ba874adfd3a7f136b74c555ec3 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 11 May 2026 11:41:50 +0200 Subject: [PATCH 132/236] Update documentation for upstream and downstream regions --- docs/library.rst | 62 +++++++++++++++++++----------------------------- 1 file changed, 25 insertions(+), 37 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 64aa788..39681ab 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -102,26 +102,14 @@ denote upstream and ``d`` to denote downstream. .. code:: python >>> crossmap.coordinate_to_noncoding(2) - {'position': 3, 'offset': 0, 'region': 'u'} - >>> crossmap.noncoding_to_coordinate({'position': 3, 'offset': 0, 'region': 'u'}) + {'position': 1, 'offset': -3, 'region': 'u'} + >>> crossmap.noncoding_to_coordinate({'position': 1, 'offset': -3, 'region': 'u'}) 2 >>> crossmap.coordinate_to_noncoding(73) - {'position': 2, 'offset': 0, 'region': 'd'} - >>> crossmap.noncoding_to_coordinate({'position': 2, 'offset': 0, 'region': 'd'}) + {'position': 22, 'offset': 2, 'region': 'd'} + >>> crossmap.noncoding_to_coordinate({'position': 22, 'offset': 2, 'region': 'd'}) 73 -The ``coordinate_to_noncoding()`` function accepts an optional ``degenerate`` -argument. When set to ``True``, positions outside of the transcript are no -longer described using the ``u`` or ``d`` notation, ``-`` and ``*`` are used -instead. The values in ``position`` and ``offset`` will change accordingly. - -.. code:: python - - >>> crossmap.coordinate_to_noncoding(2) - {'position': 3, 'offset': 0, 'region': 'u'} - >>> crossmap.coordinate_to_noncoding(2, True) - {'position': 1, 'offset': -3, 'region': '-'} - For transcripts that reside on the reverse complement strand, the ``inverted`` parameter should be set to ``True``. In our example, HGVS position ``g.36`` (coordinate *35*) is now equivalent to position ``n.9-1``. @@ -146,13 +134,13 @@ In the following table, we show a number of annotated examples. - region - HGVS * - 0 - - 5 - - 0 + - 1 + - -5 - ``u`` - ``n.u5`` * - 4 - 1 - - 0 + - -1 - ``u`` - ``n.u1`` * - 5 @@ -176,13 +164,13 @@ In the following table, we show a number of annotated examples. - - ``n.22`` * - 72 + - 22 - 1 - - 0 - ``d`` - ``n.d1`` * - 79 + - 22 - 8 - - 0 - ``d`` - ``n.d8`` @@ -243,12 +231,12 @@ position ``c.-1``. We can convert between these two as follows. The ``coordinate_to_coding()`` function accepts an optional ``degenerate`` argument. When set to ``True``, positions outside of the transcript are no longer described using the ``u`` or ``d`` notation, ``-`` and ``*`` are used -instead. Note that the value of ``position`` is adjusted accordingly. +instead. Note that the values of ``position`` and ``offset`` are adjusted accordingly. .. code:: python >>> crossmap.coordinate_to_coding(4) - {'position': 1, 'offset': 0, 'region': 'u'} + {'position': 11, 'offset': -1, 'region': 'u'} >>> crossmap.coordinate_to_coding(4, True) {'position': 12, 'offset': 0, 'region': '-'} @@ -264,13 +252,13 @@ In the following table, we show a number of annotated examples. - region - HGVS * - 0 - - 5 - - 0 + - 11 + - -5 - ``u`` - ``c.u5`` * - 4 - - 1 - - 0 + - 11 + - -1 - ``u`` - ``c.u1`` * - 5 @@ -319,13 +307,13 @@ In the following table, we show a number of annotated examples. - ``*`` - ``c.*5`` * - 72 + - 5 - 1 - - 0 - ``d`` - ``c.d1`` * - 79 + - 5 - 8 - - 0 - ``d`` - ``c.d8`` @@ -379,15 +367,15 @@ table, we show a number of annotated examples. - region - HGVS * - 0 + - 4 - 2 - - 2 - - 0 + - -5 - ``u`` - * - 4 - - 1 - - 3 - - 0 + - 4 + - 2 + - -1 - ``u`` - * - 5 @@ -445,16 +433,16 @@ table, we show a number of annotated examples. - ``*`` - * - 72 + - 2 + - 2 - 1 - - 1 - - 0 - ``d`` - * - 79 - 2 - 2 - - 0 + - 8 - ``d`` - From 3f18bdb87058c0be033fdbef0461286b4a4f7041 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 12 May 2026 15:12:45 +0200 Subject: [PATCH 133/236] Reshape outputs from downstream regions and add tests. --- mutalyzer_crossmapper/crossmapper.py | 6 +- tests/test_crossmapper.py | 106 ++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index fb029b7..1333748 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -109,7 +109,7 @@ def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: offset = multilocus_pos_m['offset'] region = multilocus_pos_m['region'] if region=='u': - if self._coding[0] == 0: + if self._exons[0] == self._coding[0]: return { 'position': 1, 'offset': offset, @@ -166,14 +166,14 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dic degenerate_pos_m = {**pos_m, 'offset': pos_m['offset']} if region == 'u': if self._coding[0] == 0: - degenerate_pos_m['position'] = degenerate_pos_m['position'] + abs(degenerate_pos_m['offset']) - 1 + degenerate_pos_m['position'] = abs(degenerate_pos_m['offset']) else: degenerate_pos_m['position'] = degenerate_pos_m['position'] + abs(degenerate_pos_m['offset']) degenerate_pos_m['offset'] = 0 degenerate_pos_m['region'] = '-' if region == 'd': if self._exons[1] == self._coding[1] : - degenerate_pos_m['position'] = degenerate_pos_m['position'] + abs(degenerate_pos_m['offset']) - 1 + degenerate_pos_m['position'] = abs(degenerate_pos_m['offset']) else: degenerate_pos_m['position'] = degenerate_pos_m['position'] + abs(degenerate_pos_m['offset']) degenerate_pos_m['offset'] = 0 diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 21c5bbb..b7b0be1 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -602,6 +602,110 @@ def test_Coding_inverted_degenerate_return(): } +def test_Coding_no_utr5_degenerate_return(): + """A 5' UTR may be missing.""" + crossmap = Coding([(10, 20)], (10, 15)) + + assert crossmap.coordinate_to_coding(9, True) == { + 'position': 1, + 'offset': 0, + 'region': '-' + } + assert crossmap.coordinate_to_coding(10, True) == { + 'position': 1, + 'offset': 0, + 'region': '' + } + assert crossmap.coordinate_to_coding(19, True) == { + 'position': 5, + 'offset': 0, + 'region': '*' + } + assert crossmap.coordinate_to_coding(20, True) == { + 'position': 6, + 'offset': 0, + 'region': '*' + } + + +def test_Coding_no_utr5_inverted_degenerate_return(): + """A 5' UTR may be missing.""" + crossmap = Coding([(10, 20)], (10, 15), True) + + assert crossmap.coordinate_to_coding(9, True) == { + 'position': 1, + 'offset': 0, + 'region': '*' + } + assert crossmap.coordinate_to_coding(10, True) == { + 'position': 5, + 'offset': 0, + 'region': '' + } + assert crossmap.coordinate_to_coding(19, True) == { + 'position': 5, + 'offset': 0, + 'region': '-' + } + assert crossmap.coordinate_to_coding(20, True) == { + 'position': 6, + 'offset': 0, + 'region': '-' + } + + +def test_Coding_no_utr3_degenerate_return(): + """A 3' UTR may be missing.""" + crossmap = Coding([(10, 20)], (15, 20)) + + assert crossmap.coordinate_to_coding(9, True) == { + 'position': 6, + 'offset': 0, + 'region': '-' + } + assert crossmap.coordinate_to_coding(10, True) == { + 'position': 5, + 'offset': 0, + 'region': '-', + } + assert crossmap.coordinate_to_coding(19, True) == { + 'position': 5, + 'offset': 0, + 'region': '', + } + assert crossmap.coordinate_to_coding(20, True) == { + 'position': 1, + 'offset': 0, + 'region': '*', + } + + +def test_Coding_no_utr3_inverted_degenerate_return(): + """A 3' UTR may be missing.""" + crossmap = Coding([(10, 20)], (15, 20), True) + + assert crossmap.coordinate_to_coding(9, True) == { + 'position': 6, + 'offset': 0, + 'region': '*' + } + assert crossmap.coordinate_to_coding(10, True) == { + 'position': 5, + 'offset': 0, + 'region': '*', + } + assert crossmap.coordinate_to_coding(19, True) == { + 'position': 1, + 'offset': 0, + 'region': '', + } + assert crossmap.coordinate_to_coding(20, True) == { + 'position': 1, + 'offset': 0, + 'region': '-', + } + + def test_Coding_two_exons_inverted_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" crossmap = Coding([(10, 20), (30, 40)], (18, 37), True) @@ -627,6 +731,7 @@ def test_Coding_two_exons_inverted_degenerate_return(): 'region': '-', } + def test_Coding_degenerate_no_return(): """Degenerate internal positions do not exist.""" crossmap = Coding([(10, 20), (30, 40)], (10, 40)) @@ -684,7 +789,6 @@ def test_Coding_inverted_no_utr_degenerate(): {'position': 1, 'offset': -1, 'region': 'd'}, ], ) - print(crossmap.coding_to_coordinate({'position': 1, 'offset': 1, 'region': 'u'})) degenerate_equal( crossmap.coding_to_coordinate, 9, From f73893ca3aa01ab8b43a8907efe8057e7ba438dd Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 13 May 2026 12:21:17 +0200 Subject: [PATCH 134/236] Add tests for degenerate return in small UTR regions --- tests/test_crossmapper.py | 84 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index b7b0be1..80394a1 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -706,6 +706,90 @@ def test_Coding_no_utr3_inverted_degenerate_return(): } +def test_Coding_small_utr5_degenerate_return(): + """A 5' UTR may be of lenght one.""" + crossmap = Coding([(10, 20)], (11, 15)) + + assert crossmap.coordinate_to_coding(9, True) == { + 'position': 2, + 'offset': 0, + 'region': '-' + } + assert crossmap.coordinate_to_coding(10, True) == { + 'position': 1, + 'offset': 0, + 'region': '-' + } + assert crossmap.coordinate_to_coding(11, True) == { + 'position': 1, + 'offset': 0, + 'region': '' + } + + +def test_Coding_small_utr5_inverted_degenerate_return(): + """A 5' UTR may be of lenght one.""" + crossmap = Coding([(10, 20)], (11, 15), True) + + assert crossmap.coordinate_to_coding(9, True) == { + 'position': 2, + 'offset': 0, + 'region': '*' + } + assert crossmap.coordinate_to_coding(10, True) == { + 'position': 1, + 'offset': 0, + 'region': '*' + } + assert crossmap.coordinate_to_coding(11, True) == { + 'position': 4, + 'offset': 0, + 'region': '' + } + + +def test_Coding_small_utr3_degenerate_return(): + """A 3' UTR may be of lenght one.""" + crossmap = Coding([(10, 20)], (15, 19)) + + assert crossmap.coordinate_to_coding(18, True) == { + 'position': 4, + 'offset': 0, + 'region': '' + } + assert crossmap.coordinate_to_coding(19, True) == { + 'position': 1, + 'offset': 0, + 'region': '*' + } + assert crossmap.coordinate_to_coding(20, True) == { + 'position': 2, + 'offset': 0, + 'region': '*' + } + + +def test_Coding_small_utr3_inverted_degenerate_return(): + """A 3' UTR may be of lenght one.""" + crossmap = Coding([(10, 20)], (15, 19), True) + + assert crossmap.coordinate_to_coding(18, True) == { + 'position': 1, + 'offset': 0, + 'region': '' + } + assert crossmap.coordinate_to_coding(19, True) == { + 'position': 1, + 'offset': 0, + 'region': '-' + } + assert crossmap.coordinate_to_coding(20, True) == { + 'position': 2, + 'offset': 0, + 'region': '-' + } + + def test_Coding_two_exons_inverted_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" crossmap = Coding([(10, 20), (30, 40)], (18, 37), True) From 6cc6f5aa83967cfb406ad36bc72dd5618bab50c7 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 26 May 2026 11:46:42 +0200 Subject: [PATCH 135/236] Formatting according to pylint --- mutalyzer_crossmapper/crossmapper.py | 107 +++++++++++++-------------- 1 file changed, 51 insertions(+), 56 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 1333748..326f7b9 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -3,6 +3,7 @@ class Genomic(object): """Genomic crossmap object.""" + def coordinate_to_genomic(self, coordinate: int) -> dict[str, int]: """Convert a coordinate to a genomic position (g./m./o.). @@ -24,6 +25,7 @@ def genomic_to_coordinate(self, pos_m: dict[str, int]) -> int: class NonCoding(Genomic): """NonCoding crossmap object.""" + def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> None: """ :arg list locations: List of locus locations. @@ -33,10 +35,7 @@ def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> self._noncoding = MultiLocus(locations, inverted) - def coordinate_to_noncoding( - self, - coordinate: int, - ) -> dict[str, int | str]: + def coordinate_to_noncoding(self, coordinate: int) -> dict[str, int | str]: """Convert a coordinate to a noncoding position (n./r.). :arg int coordinate: Coordinate. @@ -75,12 +74,12 @@ def __init__( cds_start = self._noncoding.to_position(cds[0]) cds_end = self._noncoding.to_position(cds[1] - 1) exon_start = self._noncoding.to_position(locations[0][0]) - exon_end = self._noncoding.to_position(locations[-1][1] -1) + exon_end = self._noncoding.to_position(locations[-1][1] - 1) if self._inverted: self._coding = ( cds_end['position'] + cds_end['offset'], - cds_start['position'] + cds_start['offset'] + 1 + cds_start['position'] + cds_start['offset'] + 1, ) self._exons = ( exon_end['position'] + exon_end['offset'], @@ -89,13 +88,12 @@ def __init__( else: self._coding = ( cds_start['position'] + cds_start['offset'], - cds_end['position'] + cds_end['offset'] + 1 + cds_end['position'] + cds_end['offset'] + 1, ) self._exons = ( exon_start['position'] + exon_start['offset'], exon_end['position'] + exon_end['offset'] + 1, ) - def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: """Convert a coordinate to a coding position (c./r.). @@ -108,46 +106,35 @@ def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: location = multilocus_pos_m['position'] offset = multilocus_pos_m['offset'] region = multilocus_pos_m['region'] - if region=='u': + + if region == 'u': if self._exons[0] == self._coding[0]: - return { - 'position': 1, - 'offset': offset, - 'region': 'u' - } - return { - 'position':self._coding[0], - 'offset': offset, - 'region': 'u' - } - if region == 'd': + position = 1 + else: + position = self._coding[0] + + elif region == 'd': if self._exons[1] == self._coding[1]: - return { - 'position': self._coding[1] - self._coding[0], - 'offset': offset, - 'region': 'd' - } - return { - 'position': location - self._coding[1] + 1, - 'offset': offset, - 'region': 'd' - } - if location < self._coding[0]: - return { - 'position': self._coding[0] - location, - 'offset': offset, - 'region': '-' - } - if location >= self._coding[1]: - return { - 'position': location - self._coding[1] + 1, - 'offset': offset, - 'region': '*' - } + position = self._coding[1] - self._coding[0] + else: + position = location - self._coding[1] + 1 + + elif location < self._coding[0]: + position = self._coding[0] - location + region = '-' + + elif location >= self._coding[1]: + position = location - self._coding[1] + 1 + region = '*' + + else: + position = location - self._coding[0] + 1 + region = '' + return { - 'position': location - self._coding[0] + 1, + 'position': position, 'offset': offset, - 'region': '' + 'region': region, } def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dict: @@ -172,7 +159,7 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dic degenerate_pos_m['offset'] = 0 degenerate_pos_m['region'] = '-' if region == 'd': - if self._exons[1] == self._coding[1] : + if self._exons[1] == self._coding[1]: degenerate_pos_m['position'] = abs(degenerate_pos_m['offset']) else: degenerate_pos_m['position'] = degenerate_pos_m['position'] + abs(degenerate_pos_m['offset']) @@ -219,12 +206,14 @@ def coordinate_to_protein(self, coordinate: int) -> dict[str, int | str]: 'position': abs(-location // 3), 'position_in_codon': -location % 3 + 1, 'region': pos_m['region'], - 'offset': pos_m['offset']} + 'offset': pos_m['offset'], + } return { - 'position': (location + 2) // 3, - 'position_in_codon': (location + 2) % 3 + 1, - 'region': pos_m['region'], - 'offset': pos_m['offset']} + 'position': (location + 2) // 3, + 'position_in_codon': (location + 2) % 3 + 1, + 'region': pos_m['region'], + 'offset': pos_m['offset'], + } def protein_to_coordinate(self, pos_m: dict[str, int | str]) -> int: """Convert a protein position (p.) to a coordinate. @@ -235,11 +224,17 @@ def protein_to_coordinate(self, pos_m: dict[str, int | str]) -> int: """ if pos_m['region'] in ('-', 'u'): return self.coding_to_coordinate( - {'position': 3 * pos_m['position'] - pos_m['position_in_codon'] + 1, - 'offset': pos_m['offset'], - 'region': pos_m['region']}) + { + 'position': 3 * pos_m['position'] - pos_m['position_in_codon'] + 1, + 'offset': pos_m['offset'], + 'region': pos_m['region'], + } + ) return self.coding_to_coordinate( - {'position': 3 * pos_m['position'] + pos_m['position_in_codon'] - 3, - 'offset': pos_m['offset'], - 'region': pos_m['region']}) + { + 'position': 3 * pos_m['position'] + pos_m['position_in_codon'] - 3, + 'offset': pos_m['offset'], + 'region': pos_m['region'], + } + ) From ebd0cd35db9fc5511fc690d57a995cf88d9bc3e4 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 26 May 2026 11:59:33 +0200 Subject: [PATCH 136/236] Formatting. --- mutalyzer_crossmapper/crossmapper.py | 1 + tests/helper.py | 2 +- tests/test_crossmapper.py | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 326f7b9..e58f95a 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -94,6 +94,7 @@ def __init__( exon_start['position'] + exon_start['offset'], exon_end['position'] + exon_end['offset'] + 1, ) + def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: """Convert a coordinate to a coding position (c./r.). diff --git a/tests/helper.py b/tests/helper.py index b3ca042..a17b119 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -6,4 +6,4 @@ def invariant(f, x, f_i, y): def degenerate_equal(f, coordinate, locations): assert f(locations[0]) == coordinate assert len( - set(map(f, locations))) == 1 \ No newline at end of file + set(map(f, locations))) == 1 diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 80394a1..bc526c4 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -861,7 +861,6 @@ def test_Coding_inverted_no_utr_degenerate(): """UTRs may be missing.""" crossmap = Coding([(10, 11)], (10, 11), True) - degenerate_equal( crossmap.coding_to_coordinate, 11, From 562d7b86bddc2cbdba3969479a7af373076b4f4a Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 26 May 2026 12:03:02 +0200 Subject: [PATCH 137/236] Remove git conflict. --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 1075310..381420f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,3 @@ docs/_build/ mutalyzer_crossmapper.egg-info/ mutalyzer_crossmapper/__pycache__/ tests/__pycache__/ -<<<<<<< Updated upstream -======= -tmp/ ->>>>>>> Stashed changes From 136bb18ebe19debc973e18271152ad870acc707a Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 26 May 2026 16:51:18 +0200 Subject: [PATCH 138/236] Discard local variable for degenerate and handle '*' explicitly. --- mutalyzer_crossmapper/crossmapper.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index e58f95a..69c0fb0 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -151,22 +151,21 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dic region = pos_m['region'] if not degenerate: return pos_m - degenerate_pos_m = {**pos_m, 'offset': pos_m['offset']} if region == 'u': if self._coding[0] == 0: - degenerate_pos_m['position'] = abs(degenerate_pos_m['offset']) + pos_m['position'] = abs(pos_m['offset']) else: - degenerate_pos_m['position'] = degenerate_pos_m['position'] + abs(degenerate_pos_m['offset']) - degenerate_pos_m['offset'] = 0 - degenerate_pos_m['region'] = '-' + pos_m['position'] = pos_m['position'] + abs(pos_m['offset']) + pos_m['offset'] = 0 + pos_m['region'] = '-' if region == 'd': if self._exons[1] == self._coding[1]: - degenerate_pos_m['position'] = abs(degenerate_pos_m['offset']) + pos_m['position'] = abs(pos_m['offset']) else: - degenerate_pos_m['position'] = degenerate_pos_m['position'] + abs(degenerate_pos_m['offset']) - degenerate_pos_m['offset'] = 0 - degenerate_pos_m['region'] = '*' - return degenerate_pos_m + pos_m['position'] = pos_m['position'] + abs(pos_m['offset']) + pos_m['offset'] = 0 + pos_m['region'] = '*' + return pos_m def coding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: """Convert a coding position (c./r.) to a coordinate. @@ -180,7 +179,6 @@ def coding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: multilocus_pos_m = {**pos_m} if region in ('u', 'd'): - multilocus_pos_m['position'] = location - 1 return self._noncoding.to_coordinate(multilocus_pos_m) multilocus_pos_m['region'] = '' @@ -188,7 +186,7 @@ def coding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: multilocus_pos_m['position'] = location + self._coding[0] - 1 elif region == '-': multilocus_pos_m['position'] = self._coding[0] - location - else: + elif region == '*': multilocus_pos_m['position'] = self._coding[1] + location - 1 return self._noncoding.to_coordinate(multilocus_pos_m) From 327c7ddc52f2356b55602503797ac5099ef3e863 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 26 May 2026 16:58:47 +0200 Subject: [PATCH 139/236] Fix typo in tests. --- tests/test_crossmapper.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index bc526c4..e9116e7 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -405,7 +405,7 @@ def test_Coding_no_utr3_inverted(): def test_Coding_small_utr5(): - """A 5' UTR may be of lenght one.""" + """A 5' UTR may be of length one.""" crossmap = Coding([(10, 20)], (11, 15)) # Transition from upstream to 5' UTR to CDS. @@ -430,7 +430,7 @@ def test_Coding_small_utr5(): def test_Coding_small_utr5_inverted(): - """A 5' UTR may be of lenght one.""" + """A 5' UTR may be of length one.""" crossmap = Coding([(10, 20)], (15, 19), True) # Transition from upstream to 5' UTR to CDS. @@ -455,7 +455,7 @@ def test_Coding_small_utr5_inverted(): def test_Coding_small_utr3(): - """A 5' UTR may be of lenght one.""" + """A 5' UTR may be of length one.""" crossmap = Coding([(10, 20)], (15, 19)) # Transition from CDS to 3' UTR to downstream. @@ -480,7 +480,7 @@ def test_Coding_small_utr3(): def test_Coding_small_utr3_inverted(): - """A 5' UTR may be of lenght one.""" + """A 5' UTR may be of length one.""" crossmap = Coding([(10, 20)], (11, 15), True) # Transition from CDS to 3' UTR to downstream. @@ -707,7 +707,7 @@ def test_Coding_no_utr3_inverted_degenerate_return(): def test_Coding_small_utr5_degenerate_return(): - """A 5' UTR may be of lenght one.""" + """A 5' UTR may be of length one.""" crossmap = Coding([(10, 20)], (11, 15)) assert crossmap.coordinate_to_coding(9, True) == { @@ -728,7 +728,7 @@ def test_Coding_small_utr5_degenerate_return(): def test_Coding_small_utr5_inverted_degenerate_return(): - """A 5' UTR may be of lenght one.""" + """A 5' UTR may be of length one.""" crossmap = Coding([(10, 20)], (11, 15), True) assert crossmap.coordinate_to_coding(9, True) == { @@ -749,7 +749,7 @@ def test_Coding_small_utr5_inverted_degenerate_return(): def test_Coding_small_utr3_degenerate_return(): - """A 3' UTR may be of lenght one.""" + """A 3' UTR may be of length one.""" crossmap = Coding([(10, 20)], (15, 19)) assert crossmap.coordinate_to_coding(18, True) == { @@ -770,7 +770,7 @@ def test_Coding_small_utr3_degenerate_return(): def test_Coding_small_utr3_inverted_degenerate_return(): - """A 3' UTR may be of lenght one.""" + """A 3' UTR may be of length one.""" crossmap = Coding([(10, 20)], (15, 19), True) assert crossmap.coordinate_to_coding(18, True) == { From 67f72422287ad1bdefe64fab70ec6ff6c5efaa47 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 27 May 2026 15:23:27 +0200 Subject: [PATCH 140/236] Formatting locus module. --- mutalyzer_crossmapper/locus.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 7fbb7b3..c9bf9ec 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -1,6 +1,6 @@ class Locus(object): """Locus object.""" - def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: + def __init__(self, location: tuple[int, int], inverted: bool=False) -> None: """ :arg tuple location: Locus location. :arg bool inverted: Orientation. @@ -30,13 +30,13 @@ def to_position(self, coordinate: int) -> dict[str, int]: return {'position': self._end, 'offset': coordinate - self.boundary[1]} return {'position': coordinate - self.boundary[0], 'offset': 0} - def to_coordinate(self, pos_m: dict[str, int]) -> int: + def to_coordinate(self, point: dict[str, int]) -> int: """Convert a position model to a coordinate. - :arg dict position: Position model with 'position' and 'offset' keys. + :arg dict point: Position model with 'position' and 'offset' keys. :returns int: Coordinate. """ if self._inverted: - return self.boundary[1] - pos_m['position'] - pos_m['offset'] - return self.boundary[0] + pos_m['position'] + pos_m['offset'] + return self.boundary[1] - point['position'] - point['offset'] + return self.boundary[0] + point['position'] + point['offset'] From b4f3f846aae0bd218fffe98ea9a2066bf4742c26 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 27 May 2026 15:26:38 +0200 Subject: [PATCH 141/236] Formatting and renaming local variables. --- mutalyzer_crossmapper/multi_locus.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 165f722..5a7678a 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -19,7 +19,7 @@ def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: class MultiLocus(object): """MultiLocus object.""" - def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> None: + def __init__(self, locations: list[tuple[int, int]], inverted: bool=False) -> None: """ :arg list locations: List of locus locations. :arg bool inverted: Orientation. @@ -59,34 +59,34 @@ def to_position(self, coordinate: int) -> dict[str, int | str]: index = nearest_location(self._locations, coordinate, self._inverted) outside = self._orientation * self.outside(coordinate) region = 'u' if outside < 0 else 'd' if outside > 0 else '' - locus_pos_m = self._loci[index].to_position(coordinate) + point = self._loci[index].to_position(coordinate) return { - 'position': locus_pos_m['position'] + self._offsets[self._direction(index)], - 'offset': locus_pos_m['offset'], + 'position': point['position'] + self._offsets[self._direction(index)], + 'offset': point['offset'], 'region': region } - def to_coordinate(self, pos_m: dict[str, int | str]) -> int: + def to_coordinate(self, point: dict[str, int | str]) -> int: """Convert a position model to a coordinate. - :arg dict pos_m: Position model with 'position','offset' and 'region' keys. + :arg dict point: Position model with 'position','offset' and 'region' keys. :returns int: Coordinate. """ - region = pos_m['region'] + region = point['region'] if region == 'u': if self._inverted: - return self._locations[-1][1] - pos_m['offset'] - 1 - return self._locations[0][0] + pos_m['offset'] + return self._locations[-1][1] - point['offset'] - 1 + return self._locations[0][0] + point['offset'] if region == 'd': if self._inverted: - return self._locations[0][0] - pos_m['offset'] - return self._locations[-1][1] + pos_m['offset'] - 1 + return self._locations[0][0] - point['offset'] + return self._locations[-1][1] + point['offset'] - 1 index = min( len(self._offsets), - max(0, bisect_right(self._offsets, pos_m['position']) - 1) + max(0, bisect_right(self._offsets, point['position']) - 1) ) - locus_pos_m = {**pos_m, 'position': pos_m['position'] - self._offsets[index]} - return self._loci[self._direction(index)].to_coordinate(locus_pos_m) + return self._loci[self._direction(index)].to_coordinate( + {**point, 'position': point['position'] - self._offsets[index]}) From d4aea66d96b3ebecc7cf6de8df3dab4499b42033 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 27 May 2026 15:49:12 +0200 Subject: [PATCH 142/236] Crossmapper:Formatting and renaming local variables. --- mutalyzer_crossmapper/crossmapper.py | 130 +++++++++++++-------------- 1 file changed, 64 insertions(+), 66 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 69c0fb0..c901906 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -1,6 +1,5 @@ from .multi_locus import MultiLocus - class Genomic(object): """Genomic crossmap object.""" @@ -13,20 +12,20 @@ def coordinate_to_genomic(self, coordinate: int) -> dict[str, int]: """ return {'position': coordinate + 1} - def genomic_to_coordinate(self, pos_m: dict[str, int]) -> int: + def genomic_to_coordinate(self, point: dict[str, int]) -> int: """Convert a genomic position (g./m./o.) to a coordinate. - :arg dict pos_m: Genomic position model. + :arg dict point: Genomic position model. :returns int: Coordinate. """ - return pos_m['position'] - 1 + return point['position'] - 1 class NonCoding(Genomic): """NonCoding crossmap object.""" - def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> None: + def __init__(self, locations: list[tuple[int, int]], inverted: bool=False) -> None: """ :arg list locations: List of locus locations. :arg bool inverted: Orientation. @@ -42,18 +41,17 @@ def coordinate_to_noncoding(self, coordinate: int) -> dict[str, int | str]: :returns dict: Noncoding position model. """ - multilocus_pos_m = self._noncoding.to_position(coordinate) - return {**multilocus_pos_m, 'position': multilocus_pos_m['position'] + 1} + point = self._noncoding.to_position(coordinate) + return {**point, 'position': point['position'] + 1} - def noncoding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: + def noncoding_to_coordinate(self, point: dict[str, int | str]) -> int: """Convert a noncoding position (n./r.) to a coordinate. - :arg dict pos_m: Noncoding position model. + :arg dict point: Noncoding position model. :returns int: Coordinate. """ - multilocus_pos_m = {**pos_m, 'position': pos_m['position'] - 1} - return self._noncoding.to_coordinate(multilocus_pos_m) + return self._noncoding.to_coordinate({**point, 'position': point['position'] - 1}) class Coding(NonCoding): @@ -102,11 +100,11 @@ def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: :returns dict: Coding position model (c./r.). """ - multilocus_pos_m = self._noncoding.to_position(coordinate) + noncoding_point = self._noncoding.to_position(coordinate) - location = multilocus_pos_m['position'] - offset = multilocus_pos_m['offset'] - region = multilocus_pos_m['region'] + position = noncoding_point['position'] + offset = noncoding_point['offset'] + region = noncoding_point['region'] if region == 'u': if self._exons[0] == self._coding[0]: @@ -118,18 +116,18 @@ def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: if self._exons[1] == self._coding[1]: position = self._coding[1] - self._coding[0] else: - position = location - self._coding[1] + 1 + position = position - self._coding[1] + 1 - elif location < self._coding[0]: - position = self._coding[0] - location + elif position < self._coding[0]: + position = self._coding[0] - position region = '-' - elif location >= self._coding[1]: - position = location - self._coding[1] + 1 + elif position >= self._coding[1]: + position = position - self._coding[1] + 1 region = '*' else: - position = location - self._coding[0] + 1 + position = position - self._coding[0] + 1 region = '' return { @@ -138,7 +136,7 @@ def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: 'region': region, } - def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dict: + def coordinate_to_coding(self, coordinate: int, degenerate: bool=False) -> dict: """Convert a coordinate to a coding position (c./r.). :arg int coordinate: Coordinate. @@ -146,49 +144,49 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dic :returns dict: Coding position model (c./r.). """ - pos_m = self._coordinate_to_coding(coordinate) + point = self._coordinate_to_coding(coordinate) - region = pos_m['region'] + region = point['region'] if not degenerate: - return pos_m + return point if region == 'u': if self._coding[0] == 0: - pos_m['position'] = abs(pos_m['offset']) + point['position'] = abs(point['offset']) else: - pos_m['position'] = pos_m['position'] + abs(pos_m['offset']) - pos_m['offset'] = 0 - pos_m['region'] = '-' - if region == 'd': + point['position'] = point['position'] + abs(point['offset']) + point['offset'] = 0 + point['region'] = '-' + elif region == 'd': if self._exons[1] == self._coding[1]: - pos_m['position'] = abs(pos_m['offset']) + point['position'] = abs(point['offset']) else: - pos_m['position'] = pos_m['position'] + abs(pos_m['offset']) - pos_m['offset'] = 0 - pos_m['region'] = '*' - return pos_m + point['position'] = point['position'] + abs(point['offset']) + point['offset'] = 0 + point['region'] = '*' + return point - def coding_to_coordinate(self, pos_m: dict[str, int | str]) -> int: + def coding_to_coordinate(self, point: dict[str, int | str]) -> int: """Convert a coding position (c./r.) to a coordinate. - :arg dict pos_m: Coding position model (c./r.). + :arg dict point: Coding position model (c./r.). :returns int: Coordinate. """ - location = pos_m['position'] - region = pos_m['region'] - multilocus_pos_m = {**pos_m} + + region = point['region'] if region in ('u', 'd'): - return self._noncoding.to_coordinate(multilocus_pos_m) + return self._noncoding.to_coordinate(point) - multilocus_pos_m['region'] = '' + position = point['position'] + noncoding_point = {**point, 'region': ''} if region == '': - multilocus_pos_m['position'] = location + self._coding[0] - 1 + noncoding_point['position'] = position + self._coding[0] - 1 elif region == '-': - multilocus_pos_m['position'] = self._coding[0] - location + noncoding_point['position'] = self._coding[0] - position elif region == '*': - multilocus_pos_m['position'] = self._coding[1] + location - 1 - return self._noncoding.to_coordinate(multilocus_pos_m) + noncoding_point['position'] = self._coding[1] + position - 1 + return self._noncoding.to_coordinate(noncoding_point) def coordinate_to_protein(self, coordinate: int) -> dict[str, int | str]: """Convert a coordinate to a protein position (p.). @@ -197,43 +195,43 @@ def coordinate_to_protein(self, coordinate: int) -> dict[str, int | str]: :returns dict: Protein position model(p.). """ - pos_m = self.coordinate_to_coding(coordinate) + point = self.coordinate_to_coding(coordinate) - location = pos_m['position'] - if pos_m['region'] in ('-', 'u'): + position = point['position'] + if point['region'] in ('-', 'u'): return { - 'position': abs(-location // 3), - 'position_in_codon': -location % 3 + 1, - 'region': pos_m['region'], - 'offset': pos_m['offset'], + 'position': abs(-position // 3), + 'position_in_codon': -position % 3 + 1, + 'region': point['region'], + 'offset': point['offset'], } return { - 'position': (location + 2) // 3, - 'position_in_codon': (location + 2) % 3 + 1, - 'region': pos_m['region'], - 'offset': pos_m['offset'], + 'position': (position + 2) // 3, + 'position_in_codon': (position + 2) % 3 + 1, + 'region': point['region'], + 'offset': point['offset'], } - def protein_to_coordinate(self, pos_m: dict[str, int | str]) -> int: + def protein_to_coordinate(self, point: dict[str, int | str]) -> int: """Convert a protein position (p.) to a coordinate. - :arg dict pos_m: Protein position model(p.). + :arg dict point: Protein position model(p.). :returns int: Coordinate. """ - if pos_m['region'] in ('-', 'u'): + if point['region'] in ('-', 'u'): return self.coding_to_coordinate( { - 'position': 3 * pos_m['position'] - pos_m['position_in_codon'] + 1, - 'offset': pos_m['offset'], - 'region': pos_m['region'], + 'position': 3 * point['position'] - point['position_in_codon'] + 1, + 'offset': point['offset'], + 'region': point['region'], } ) return self.coding_to_coordinate( { - 'position': 3 * pos_m['position'] + pos_m['position_in_codon'] - 3, - 'offset': pos_m['offset'], - 'region': pos_m['region'], + 'position': 3 * point['position'] + point['position_in_codon'] - 3, + 'offset': point['offset'], + 'region': point['region'], } ) From 935bbdb8463ad9369226194c84d6fa99437c0149 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 29 May 2026 13:29:37 +0200 Subject: [PATCH 143/236] Rename function to_position to to_point and add whitespace around = in typed params. --- mutalyzer_crossmapper/locus.py | 12 ++++++------ mutalyzer_crossmapper/multi_locus.py | 21 +++++++++++++-------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index c9bf9ec..c3a722b 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -1,6 +1,6 @@ class Locus(object): """Locus object.""" - def __init__(self, location: tuple[int, int], inverted: bool=False) -> None: + def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: """ :arg tuple location: Locus location. :arg bool inverted: Orientation. @@ -10,12 +10,12 @@ def __init__(self, location: tuple[int, int], inverted: bool=False) -> None: self.boundary = location[0], location[1] - 1 self._end = self.boundary[1] - self.boundary[0] - def to_position(self, coordinate: int) -> dict[str, int]: - """Convert a coordinate to a proper position model. + def to_point(self, coordinate: int) -> dict[str, int]: + """Convert a coordinate to a proper point model. :arg int coordinate: Coordinate. - :returns dict: Position model with 'position' and 'offset' keys. + :returns dict: Point model with 'position' and 'offset' keys. """ if self._inverted: if coordinate > self.boundary[1]: @@ -31,9 +31,9 @@ def to_position(self, coordinate: int) -> dict[str, int]: return {'position': coordinate - self.boundary[0], 'offset': 0} def to_coordinate(self, point: dict[str, int]) -> int: - """Convert a position model to a coordinate. + """Convert a point model to a coordinate. - :arg dict point: Position model with 'position' and 'offset' keys. + :arg dict point: Point model with 'position' and 'offset' keys. :returns int: Coordinate. """ diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 5a7678a..9cf8ab4 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -19,7 +19,7 @@ def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: class MultiLocus(object): """MultiLocus object.""" - def __init__(self, locations: list[tuple[int, int]], inverted: bool=False) -> None: + def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> None: """ :arg list locations: List of locus locations. :arg bool inverted: Orientation. @@ -49,17 +49,17 @@ def outside(self, coordinate: int) -> int: return coordinate - self._loci[-1].boundary[1] return 0 - def to_position(self, coordinate: int) -> dict[str, int | str]: - """Convert a coordinate to a position. + def to_point(self, coordinate: int) -> dict[str, int | str]: + """Convert a coordinate to a point. :arg int coordinate: Coordinate. - :returns dict: Position model 'position', 'offset' and 'region' keys. + :returns dict: Point model 'position', 'offset' and 'region' keys. """ index = nearest_location(self._locations, coordinate, self._inverted) outside = self._orientation * self.outside(coordinate) region = 'u' if outside < 0 else 'd' if outside > 0 else '' - point = self._loci[index].to_position(coordinate) + point = self._loci[index].to_point(coordinate) return { 'position': point['position'] + self._offsets[self._direction(index)], 'offset': point['offset'], @@ -67,9 +67,9 @@ def to_position(self, coordinate: int) -> dict[str, int | str]: } def to_coordinate(self, point: dict[str, int | str]) -> int: - """Convert a position model to a coordinate. + """Convert a point model to a coordinate. - :arg dict point: Position model with 'position','offset' and 'region' keys. + :arg dict point: Point model with 'position','offset' and 'region' keys. :returns int: Coordinate. """ @@ -89,4 +89,9 @@ def to_coordinate(self, point: dict[str, int | str]) -> int: max(0, bisect_right(self._offsets, point['position']) - 1) ) return self._loci[self._direction(index)].to_coordinate( - {**point, 'position': point['position'] - self._offsets[index]}) + { + 'position': point['position'] - self._offsets[index], + 'offset': point['offset'], + 'region': point['region'], + } + ) From a20096de43e742f3bfbcfe415875f9001c4fad56 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 29 May 2026 14:14:45 +0200 Subject: [PATCH 144/236] Undo rename from to_position to to_point and update format. --- mutalyzer_crossmapper/crossmapper.py | 45 +++++++++++++--------------- mutalyzer_crossmapper/locus.py | 2 +- mutalyzer_crossmapper/multi_locus.py | 8 ++--- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index c901906..62f8ba1 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -1,21 +1,22 @@ from .multi_locus import MultiLocus + class Genomic(object): """Genomic crossmap object.""" def coordinate_to_genomic(self, coordinate: int) -> dict[str, int]: - """Convert a coordinate to a genomic position (g./m./o.). + """Convert a coordinate to a genomic point model (g./m./o.). :arg int coordinate: Coordinate. - :returns dict: Genomic position model. + :returns dict: Genomic point model. """ return {'position': coordinate + 1} def genomic_to_coordinate(self, point: dict[str, int]) -> int: - """Convert a genomic position (g./m./o.) to a coordinate. + """Convert a genomic point (g./m./o.) to a coordinate. - :arg dict point: Genomic position model. + :arg dict point: Genomic point model. :returns int: Coordinate. """ @@ -25,7 +26,7 @@ def genomic_to_coordinate(self, point: dict[str, int]) -> int: class NonCoding(Genomic): """NonCoding crossmap object.""" - def __init__(self, locations: list[tuple[int, int]], inverted: bool=False) -> None: + def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> None: """ :arg list locations: List of locus locations. :arg bool inverted: Orientation. @@ -35,23 +36,29 @@ def __init__(self, locations: list[tuple[int, int]], inverted: bool=False) -> No self._noncoding = MultiLocus(locations, inverted) def coordinate_to_noncoding(self, coordinate: int) -> dict[str, int | str]: - """Convert a coordinate to a noncoding position (n./r.). + """Convert a coordinate to a noncoding point model (n./r.). :arg int coordinate: Coordinate. - :returns dict: Noncoding position model. + :returns dict: Noncoding point model. """ point = self._noncoding.to_position(coordinate) - return {**point, 'position': point['position'] + 1} + return {'position': point['position'] + 1, "region": point['region'], 'offset': point['offset']} def noncoding_to_coordinate(self, point: dict[str, int | str]) -> int: - """Convert a noncoding position (n./r.) to a coordinate. + """Convert a noncoding point (n./r.) to a coordinate. - :arg dict point: Noncoding position model. + :arg dict point: Noncoding point model. :returns int: Coordinate. """ - return self._noncoding.to_coordinate({**point, 'position': point['position'] - 1}) + return self._noncoding.to_coordinate( + { + 'position': point['position'] - 1, + 'region': point['region'], + 'offset': point['offset'], + } + ) class Coding(NonCoding): @@ -111,32 +118,23 @@ def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: position = 1 else: position = self._coding[0] - elif region == 'd': if self._exons[1] == self._coding[1]: position = self._coding[1] - self._coding[0] else: position = position - self._coding[1] + 1 - elif position < self._coding[0]: position = self._coding[0] - position region = '-' - elif position >= self._coding[1]: position = position - self._coding[1] + 1 region = '*' - else: position = position - self._coding[0] + 1 region = '' + return {'position': position, 'offset': offset, 'region': region} - return { - 'position': position, - 'offset': offset, - 'region': region, - } - - def coordinate_to_coding(self, coordinate: int, degenerate: bool=False) -> dict: + def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dict: """Convert a coordinate to a coding position (c./r.). :arg int coordinate: Coordinate. @@ -179,7 +177,7 @@ def coding_to_coordinate(self, point: dict[str, int | str]) -> int: return self._noncoding.to_coordinate(point) position = point['position'] - noncoding_point = {**point, 'region': ''} + noncoding_point = {'position': point['position'], 'region': '', 'offset': point['offset']} if region == '': noncoding_point['position'] = position + self._coding[0] - 1 elif region == '-': @@ -227,7 +225,6 @@ def protein_to_coordinate(self, point: dict[str, int | str]) -> int: 'region': point['region'], } ) - return self.coding_to_coordinate( { 'position': 3 * point['position'] + point['position_in_codon'] - 3, diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index c3a722b..03cdce1 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -10,7 +10,7 @@ def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: self.boundary = location[0], location[1] - 1 self._end = self.boundary[1] - self.boundary[0] - def to_point(self, coordinate: int) -> dict[str, int]: + def to_position(self, coordinate: int) -> dict[str, int]: """Convert a coordinate to a proper point model. :arg int coordinate: Coordinate. diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 9cf8ab4..b9534ae 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -49,8 +49,8 @@ def outside(self, coordinate: int) -> int: return coordinate - self._loci[-1].boundary[1] return 0 - def to_point(self, coordinate: int) -> dict[str, int | str]: - """Convert a coordinate to a point. + def to_position(self, coordinate: int) -> dict[str, int | str]: + """Convert a coordinate to a point model. :arg int coordinate: Coordinate. @@ -59,11 +59,11 @@ def to_point(self, coordinate: int) -> dict[str, int | str]: index = nearest_location(self._locations, coordinate, self._inverted) outside = self._orientation * self.outside(coordinate) region = 'u' if outside < 0 else 'd' if outside > 0 else '' - point = self._loci[index].to_point(coordinate) + point = self._loci[index].to_position(coordinate) return { 'position': point['position'] + self._offsets[self._direction(index)], 'offset': point['offset'], - 'region': region + 'region': region, } def to_coordinate(self, point: dict[str, int | str]) -> int: From 910c291ee76c4375ffc8224728d0d5f4fec5eebf Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 29 May 2026 14:21:33 +0200 Subject: [PATCH 145/236] Formatting --- mutalyzer_crossmapper/crossmapper.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 62f8ba1..d1421d1 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -43,7 +43,11 @@ def coordinate_to_noncoding(self, coordinate: int) -> dict[str, int | str]: :returns dict: Noncoding point model. """ point = self._noncoding.to_position(coordinate) - return {'position': point['position'] + 1, "region": point['region'], 'offset': point['offset']} + return { + 'position': point['position'] + 1, + 'region': point['region'], + 'offset': point['offset'], + } def noncoding_to_coordinate(self, point: dict[str, int | str]) -> int: """Convert a noncoding point (n./r.) to a coordinate. From d8e78344442a7a57daf70b9a1d6567b9d2acd40f Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 12 Jun 2026 13:04:56 +0200 Subject: [PATCH 146/236] Add genomic, noncoding, coding and protein dataclasses --- mutalyzer_crossmapper/__init__.py | 1 + mutalyzer_crossmapper/crossmapper.py | 145 +++++++++++++------------- mutalyzer_crossmapper/models.py | 150 +++++++++++++++++++++++++++ 3 files changed, 224 insertions(+), 72 deletions(-) create mode 100644 mutalyzer_crossmapper/models.py diff --git a/mutalyzer_crossmapper/__init__.py b/mutalyzer_crossmapper/__init__.py index 7d1abc0..fc079a8 100644 --- a/mutalyzer_crossmapper/__init__.py +++ b/mutalyzer_crossmapper/__init__.py @@ -4,6 +4,7 @@ from .location import nearest_location from .locus import Locus from .multi_locus import MultiLocus +from .models import GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint def _get_metadata(name: str) -> str: diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index d1421d1..5ea94b4 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -1,4 +1,5 @@ from .multi_locus import MultiLocus +from .models import GenomicPoint, NonCodingPoint, CodingPoint,ProteinPoint class Genomic(object): @@ -11,16 +12,16 @@ def coordinate_to_genomic(self, coordinate: int) -> dict[str, int]: :returns dict: Genomic point model. """ - return {'position': coordinate + 1} + return GenomicPoint(coordinate + 1).to_dict() - def genomic_to_coordinate(self, point: dict[str, int]) -> int: + def genomic_to_coordinate(self, point: GenomicPoint) -> int: """Convert a genomic point (g./m./o.) to a coordinate. :arg dict point: Genomic point model. :returns int: Coordinate. """ - return point['position'] - 1 + return GenomicPoint.to_dataclass(point).position - 1 class NonCoding(Genomic): @@ -42,25 +43,26 @@ def coordinate_to_noncoding(self, coordinate: int) -> dict[str, int | str]: :returns dict: Noncoding point model. """ - point = self._noncoding.to_position(coordinate) - return { - 'position': point['position'] + 1, - 'region': point['region'], - 'offset': point['offset'], - } - - def noncoding_to_coordinate(self, point: dict[str, int | str]) -> int: + point = NonCodingPoint.to_dataclass(self._noncoding.to_position(coordinate)) + return NonCodingPoint( + position=point.position + 1, + offset=point.offset, + region=point.region, + ).to_dict() + + def noncoding_to_coordinate(self, point: NonCodingPoint) -> int: """Convert a noncoding point (n./r.) to a coordinate. :arg dict point: Noncoding point model. :returns int: Coordinate. """ + noncoding_point = NonCodingPoint.to_dataclass(point) return self._noncoding.to_coordinate( { - 'position': point['position'] - 1, - 'region': point['region'], - 'offset': point['offset'], + 'position': noncoding_point.position - 1, + 'region': noncoding_point.region, + 'offset': noncoding_point.offset, } ) @@ -104,18 +106,18 @@ def __init__( exon_end['position'] + exon_end['offset'] + 1, ) - def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: + def _coordinate_to_coding(self, coordinate: int) -> CodingPoint: """Convert a coordinate to a coding position (c./r.). :arg int coordinate: Coordinate. :returns dict: Coding position model (c./r.). """ - noncoding_point = self._noncoding.to_position(coordinate) + noncoding_point = NonCodingPoint.to_dataclass(self._noncoding.to_position(coordinate)) - position = noncoding_point['position'] - offset = noncoding_point['offset'] - region = noncoding_point['region'] + position = noncoding_point.position + offset = noncoding_point.offset + region = noncoding_point.region if region == 'u': if self._exons[0] == self._coding[0]: @@ -136,9 +138,9 @@ def _coordinate_to_coding(self, coordinate: int) -> dict[str, int | str]: else: position = position - self._coding[0] + 1 region = '' - return {'position': position, 'offset': offset, 'region': region} + return CodingPoint(position=position, offset=offset, region=region) - def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dict: + def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dict[str, int | str]: """Convert a coordinate to a coding position (c./r.). :arg int coordinate: Coordinate. @@ -148,26 +150,19 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dic """ point = self._coordinate_to_coding(coordinate) - region = point['region'] + region = point.region if not degenerate: - return point - if region == 'u': - if self._coding[0] == 0: - point['position'] = abs(point['offset']) - else: - point['position'] = point['position'] + abs(point['offset']) - point['offset'] = 0 - point['region'] = '-' - elif region == 'd': - if self._exons[1] == self._coding[1]: - point['position'] = abs(point['offset']) - else: - point['position'] = point['position'] + abs(point['offset']) - point['offset'] = 0 - point['region'] = '*' - return point + return point.to_dict() - def coding_to_coordinate(self, point: dict[str, int | str]) -> int: + if region == 'u': + position = abs(point.offset) if self._coding[0] == 0 else point.position + abs(point.offset) + return CodingPoint(position=position, offset=0, region='-').to_dict() + if region == 'd': + position = abs(point.offset) if self._exons[1] == self._coding[1] else point.position + abs(point.offset) + return CodingPoint(position=position, offset=0, region='*').to_dict() + return point.to_dict() + + def coding_to_coordinate(self, point: NonCodingPoint) -> int: """Convert a coding position (c./r.) to a coordinate. :arg dict point: Coding position model (c./r.). @@ -175,13 +170,18 @@ def coding_to_coordinate(self, point: dict[str, int | str]) -> int: :returns int: Coordinate. """ - region = point['region'] + coding_point = CodingPoint.to_dataclass(point) + region = coding_point.region if region in ('u', 'd'): - return self._noncoding.to_coordinate(point) + return self._noncoding.to_coordinate(coding_point.to_dict()) - position = point['position'] - noncoding_point = {'position': point['position'], 'region': '', 'offset': point['offset']} + position = coding_point.position + noncoding_point = { + 'position': coding_point.position, + 'region': '', + 'offset': coding_point.offset, + } if region == '': noncoding_point['position'] = position + self._coding[0] - 1 elif region == '-': @@ -197,42 +197,43 @@ def coordinate_to_protein(self, coordinate: int) -> dict[str, int | str]: :returns dict: Protein position model(p.). """ - point = self.coordinate_to_coding(coordinate) - - position = point['position'] - if point['region'] in ('-', 'u'): - return { - 'position': abs(-position // 3), - 'position_in_codon': -position % 3 + 1, - 'region': point['region'], - 'offset': point['offset'], - } - return { - 'position': (position + 2) // 3, - 'position_in_codon': (position + 2) % 3 + 1, - 'region': point['region'], - 'offset': point['offset'], - } - - def protein_to_coordinate(self, point: dict[str, int | str]) -> int: + point = CodingPoint.to_dataclass(self.coordinate_to_coding(coordinate)) + + position = point.position + if point.region in ('-', 'u'): + return ProteinPoint( + position=abs(-position // 3), + position_in_codon=-position % 3 + 1, + region=point.region, + offset=point.offset, + ).to_dict() + return ProteinPoint( + position=(position + 2) // 3, + position_in_codon=(position + 2) % 3 + 1, + region=point.region, + offset=point.offset, + ).to_dict() + + def protein_to_coordinate(self, point: ProteinPoint) -> int: """Convert a protein position (p.) to a coordinate. :arg dict point: Protein position model(p.). :returns int: Coordinate. """ - if point['region'] in ('-', 'u'): + protein_point = ProteinPoint.to_dataclass(point) + if protein_point.region in ('-', 'u'): return self.coding_to_coordinate( - { - 'position': 3 * point['position'] - point['position_in_codon'] + 1, - 'offset': point['offset'], - 'region': point['region'], - } + CodingPoint( + position=3 * protein_point.position - protein_point.position_in_codon + 1, + offset=protein_point.offset, + region=protein_point.region, + ).to_dict() ) return self.coding_to_coordinate( - { - 'position': 3 * point['position'] + point['position_in_codon'] - 3, - 'offset': point['offset'], - 'region': point['region'], - } + CodingPoint( + position=3 * protein_point.position + protein_point.position_in_codon - 3, + offset=protein_point.offset, + region=protein_point.region, + ).to_dict() ) diff --git a/mutalyzer_crossmapper/models.py b/mutalyzer_crossmapper/models.py new file mode 100644 index 0000000..0208e8a --- /dev/null +++ b/mutalyzer_crossmapper/models.py @@ -0,0 +1,150 @@ +from __future__ import annotations + +from dataclasses import asdict, dataclass +from typing import Any + +NONCODING_REGIONS = {'', 'u', 'd'} +CODING_REGIONS = NONCODING_REGIONS | {'-', '*'} + + +@dataclass(slots=True) +class GenomicPoint: + position: int + + def __post_init__(self) -> None: + self._validate_position(self.position) + + @staticmethod + def _validate_position(position: int) -> None: + if not isinstance(position, int) or position < 0: + raise TypeError("position must be a non-negative integer") + + def to_dict(self) -> dict[str, Any]: + return {'position': self.position} + + @classmethod + def to_dataclass(cls, point: Any) -> GenomicPoint: + if isinstance(point, cls): + return point + if isinstance(point, dict): + return cls(position=point["position"]) + raise TypeError(f"Cannot convert {type(point)}") + + +@dataclass(slots=True) +class NonCodingPoint(GenomicPoint): + offset: int = 0 + region: str = "" + + allowed_regions = NONCODING_REGIONS + + def __post_init__(self) -> None: + GenomicPoint.__post_init__(self) + self._validate_offset(self.offset) + self._validate_region(self.region) + + @staticmethod + def _validate_offset(offset: int) -> None: + if not isinstance(offset, int): + raise TypeError("offset must be an integer") + + def _validate_region(self, region: str) -> None: + if not isinstance(region, str) or region not in self.allowed_regions: + raise ValueError(f"region must be a string in {self.allowed_regions}") + + def to_dict(self) -> dict[str, Any]: + return { + 'position': self.position, + 'offset': self.offset, + 'region': self.region, + } + + @classmethod + def to_dataclass(cls, point: Any) -> NonCodingPoint: + if isinstance(point, cls): + return point + + if isinstance(point, GenomicPoint): + return cls(position=point.position) + + if isinstance(point, dict): + return cls( + position=point["position"], + offset=point.get("offset", 0), + region=point.get("region", ""), + ) + + raise TypeError(f"Cannot convert {type(point)}") + + +@dataclass(slots=True) +class CodingPoint(NonCodingPoint): + allowed_regions = CODING_REGIONS + + @classmethod + def to_dataclass(cls, point: Any) -> CodingPoint: + if isinstance(point, cls): + return point + + if isinstance(point, NonCodingPoint): + return cls( + position=point.position, + offset=point.offset, + region=point.region, + ) + + if isinstance(point, dict): + return cls( + position=point["position"], + offset=point.get("offset", 0), + region=point.get("region", ""), + ) + + raise TypeError(f"Cannot convert {type(point)}") + + +@dataclass(slots=True) +class ProteinPoint(CodingPoint): + position_in_codon: int = 1 + + def __post_init__(self) -> None: + CodingPoint.__post_init__(self) + self._validate_position_in_codon(self.position_in_codon) + + def to_dict(self) -> dict[str, Any]: + return { + 'position': self.position, + 'offset': self.offset, + 'region': self.region, + 'position_in_codon': self.position_in_codon, + } + + @staticmethod + def _validate_position_in_codon(position_in_codon: int) -> None: + if not isinstance(position_in_codon, int) or position_in_codon not in (1, 2, 3): + raise ValueError("position_in_codon must be 1, 2, or 3") + + @classmethod + def to_dataclass(cls, point: Any) -> ProteinPoint: + if isinstance(point, cls): + return point + + if isinstance(point, CodingPoint): + return cls( + position=point.position, + offset=point.offset, + region=point.region, + position_in_codon=getattr(point, "position_in_codon", 1), + ) + + if isinstance(point, dict): + return cls( + position=point["position"], + offset=point.get("offset", 0), + region=point.get("region", ""), + position_in_codon=point.get("position_in_codon", 1), + ) + + raise TypeError(f"Cannot convert {type(point)}") + + From 34d0c9ad2ee57900a80b3cd0665da65fbe7dc548 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 12 Jun 2026 13:06:04 +0200 Subject: [PATCH 147/236] Remove tests values for noncoding degenerate. --- tests/test_crossmapper.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index e9116e7..fc09c1a 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -119,7 +119,6 @@ def test_NonCoding_degenerate(): {'position': 22, 'offset': 1, 'region': ''}, {'position': 23, 'offset': 0, 'region': ''}, {'position': 24, 'offset': -1, 'region': ''}, - {'position': 22, 'offset': 1, 'region': '*'}, ], ) @@ -135,7 +134,6 @@ def test_NonCoding_inverted_degenerate(): [ {'position': 1, 'offset': -1, 'region': ''}, {'position': 1, 'offset': -1, 'region': 'u'}, - {'position': 1, 'offset': -1, 'region': '-'}, ], ) @@ -145,7 +143,6 @@ def test_NonCoding_inverted_degenerate(): 4, [ {'position': 22, 'offset': 1, 'region': 'd'}, - {'position': 22, 'offset': 1, 'region': '*'}, {'position': 23, 'offset': 0, 'region': ''}, {'position': 22, 'offset': 1, 'region': ''}, ], From ff5c8b57294fd49d2eaf5876b778ed7b78958b5c Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 19 Jun 2026 10:05:06 +0200 Subject: [PATCH 148/236] Add dataclass internally. --- mutalyzer_crossmapper/crossmapper.py | 127 ++++++++++++++++----------- mutalyzer_crossmapper/locus.py | 26 +++--- mutalyzer_crossmapper/models.py | 72 +++++++-------- mutalyzer_crossmapper/multi_locus.py | 44 +++++----- 4 files changed, 145 insertions(+), 124 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 5ea94b4..b8d1ce2 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -5,14 +5,14 @@ class Genomic(object): """Genomic crossmap object.""" - def coordinate_to_genomic(self, coordinate: int) -> dict[str, int]: + def coordinate_to_genomic(self, coordinate: int) -> GenomicPoint: """Convert a coordinate to a genomic point model (g./m./o.). :arg int coordinate: Coordinate. - :returns dict: Genomic point model. + :returns GenomicPoint: Genomic point model. """ - return GenomicPoint(coordinate + 1).to_dict() + return GenomicPoint(coordinate + 1) def genomic_to_coordinate(self, point: GenomicPoint) -> int: """Convert a genomic point (g./m./o.) to a coordinate. @@ -36,34 +36,34 @@ def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> self._noncoding = MultiLocus(locations, inverted) - def coordinate_to_noncoding(self, coordinate: int) -> dict[str, int | str]: + def coordinate_to_noncoding(self, coordinate: int) -> NonCodingPoint: """Convert a coordinate to a noncoding point model (n./r.). :arg int coordinate: Coordinate. - :returns dict: Noncoding point model. + :returns NonCodingPoint: Noncoding point model. """ point = NonCodingPoint.to_dataclass(self._noncoding.to_position(coordinate)) return NonCodingPoint( position=point.position + 1, offset=point.offset, region=point.region, - ).to_dict() + ) def noncoding_to_coordinate(self, point: NonCodingPoint) -> int: """Convert a noncoding point (n./r.) to a coordinate. - :arg dict point: Noncoding point model. + :arg NonCodingPoint point: Noncoding point model. :returns int: Coordinate. """ noncoding_point = NonCodingPoint.to_dataclass(point) return self._noncoding.to_coordinate( - { - 'position': noncoding_point.position - 1, - 'region': noncoding_point.region, - 'offset': noncoding_point.offset, - } + NonCodingPoint( + position=noncoding_point.position - 1, + offset=noncoding_point.offset, + region=noncoding_point.region, + ) ) @@ -89,21 +89,21 @@ def __init__( if self._inverted: self._coding = ( - cds_end['position'] + cds_end['offset'], - cds_start['position'] + cds_start['offset'] + 1, + cds_end.position + cds_end.offset, + cds_start.position + cds_start.offset + 1, ) self._exons = ( - exon_end['position'] + exon_end['offset'], - exon_start['position'] + exon_start['offset'] + 1, + exon_end.position + exon_end.offset, + exon_start.position + exon_start.offset + 1, ) else: self._coding = ( - cds_start['position'] + cds_start['offset'], - cds_end['position'] + cds_end['offset'] + 1, + cds_start.position + cds_start.offset, + cds_end.position + cds_end.offset + 1, ) self._exons = ( - exon_start['position'] + exon_start['offset'], - exon_end['position'] + exon_end['offset'] + 1, + exon_start.position + exon_start.offset, + exon_end.position + exon_end.offset + 1, ) def _coordinate_to_coding(self, coordinate: int) -> CodingPoint: @@ -140,32 +140,38 @@ def _coordinate_to_coding(self, coordinate: int) -> CodingPoint: region = '' return CodingPoint(position=position, offset=offset, region=region) - def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> dict[str, int | str]: + def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> CodingPoint: """Convert a coordinate to a coding position (c./r.). :arg int coordinate: Coordinate. :arg bool degenerate: Return a degenerate position. - :returns dict: Coding position model (c./r.). + :returns CodingPoint: Coding position model (c./r.). """ point = self._coordinate_to_coding(coordinate) region = point.region if not degenerate: - return point.to_dict() + return point if region == 'u': - position = abs(point.offset) if self._coding[0] == 0 else point.position + abs(point.offset) - return CodingPoint(position=position, offset=0, region='-').to_dict() + if self._coding[0] == 0: + position = abs(point.offset) + else: + position = point.position + abs(point.offset) + return CodingPoint(position=position, offset=0, region='-') if region == 'd': - position = abs(point.offset) if self._exons[1] == self._coding[1] else point.position + abs(point.offset) - return CodingPoint(position=position, offset=0, region='*').to_dict() - return point.to_dict() + if self._exons[1] == self._coding[1]: + position = abs(point.offset) + else: + position = point.position + abs(point.offset) + return CodingPoint(position=position, offset=0, region='*') + return point - def coding_to_coordinate(self, point: NonCodingPoint) -> int: + def coding_to_coordinate(self, point: CodingPoint) -> int: """Convert a coding position (c./r.) to a coordinate. - :arg dict point: Coding position model (c./r.). + :arg CodingPoint point: Coding position model (c./r.). :returns int: Coordinate. """ @@ -174,28 +180,49 @@ def coding_to_coordinate(self, point: NonCodingPoint) -> int: region = coding_point.region if region in ('u', 'd'): - return self._noncoding.to_coordinate(coding_point.to_dict()) + return self._noncoding.to_coordinate(coding_point) position = coding_point.position - noncoding_point = { - 'position': coding_point.position, - 'region': '', - 'offset': coding_point.offset, - } if region == '': - noncoding_point['position'] = position + self._coding[0] - 1 - elif region == '-': - noncoding_point['position'] = self._coding[0] - position - elif region == '*': - noncoding_point['position'] = self._coding[1] + position - 1 - return self._noncoding.to_coordinate(noncoding_point) - - def coordinate_to_protein(self, coordinate: int) -> dict[str, int | str]: + noncoding_point = NonCodingPoint( + position=position + self._coding[0] - 1, + offset=coding_point.offset, + region='', + ) + return self._noncoding.to_coordinate(noncoding_point) + + if region == '-': + print(self._coding, position) + if position <= self._coding[0]: + return self._noncoding.to_coordinate( + NonCodingPoint( + position=self._coding[0] - position, + offset=coding_point.offset, + region='', + ) + ) + return self._noncoding.to_coordinate( + NonCodingPoint( + position=0, + offset=coding_point.offset + 1 - position, + region='u', + ) + ) + + return self._noncoding.to_coordinate( + NonCodingPoint( + position=self._coding[1] + position - 1, + offset=coding_point.offset, + region='', + ) + ) + + def coordinate_to_protein(self, coordinate: int) -> ProteinPoint: """Convert a coordinate to a protein position (p.). :arg int coordinate: Coordinate. - :returns dict: Protein position model(p.). + :returns ProteinPoint: Protein position model(p.). """ point = CodingPoint.to_dataclass(self.coordinate_to_coding(coordinate)) @@ -206,18 +233,18 @@ def coordinate_to_protein(self, coordinate: int) -> dict[str, int | str]: position_in_codon=-position % 3 + 1, region=point.region, offset=point.offset, - ).to_dict() + ) return ProteinPoint( position=(position + 2) // 3, position_in_codon=(position + 2) % 3 + 1, region=point.region, offset=point.offset, - ).to_dict() + ) def protein_to_coordinate(self, point: ProteinPoint) -> int: """Convert a protein position (p.) to a coordinate. - :arg dict point: Protein position model(p.). + :arg ProteinPoint point: Protein position model(p.). :returns int: Coordinate. """ @@ -228,12 +255,12 @@ def protein_to_coordinate(self, point: ProteinPoint) -> int: position=3 * protein_point.position - protein_point.position_in_codon + 1, offset=protein_point.offset, region=protein_point.region, - ).to_dict() + ) ) return self.coding_to_coordinate( CodingPoint( position=3 * protein_point.position + protein_point.position_in_codon - 3, offset=protein_point.offset, region=protein_point.region, - ).to_dict() + ) ) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 03cdce1..ea8a291 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -1,3 +1,5 @@ +from .models import Point + class Locus(object): """Locus object.""" def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: @@ -10,33 +12,33 @@ def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: self.boundary = location[0], location[1] - 1 self._end = self.boundary[1] - self.boundary[0] - def to_position(self, coordinate: int) -> dict[str, int]: + def to_position(self, coordinate: int) -> Point: """Convert a coordinate to a proper point model. :arg int coordinate: Coordinate. - :returns dict: Point model with 'position' and 'offset' keys. + :returns Point: Position point with 'position' and 'offset'. """ if self._inverted: if coordinate > self.boundary[1]: - return {'position': 0, 'offset': self.boundary[1] - coordinate} + return Point(position=0, offset=self.boundary[1] - coordinate) if coordinate < self.boundary[0]: - return {'position': self._end, 'offset': self.boundary[0] - coordinate} - return {'position': self.boundary[1] - coordinate, 'offset': 0} + return Point(position=self._end, offset=self.boundary[0] - coordinate) + return Point(position=self.boundary[1] - coordinate, offset=0) if coordinate < self.boundary[0]: - return {'position': 0, 'offset': coordinate - self.boundary[0]} + return Point(position=0, offset=coordinate - self.boundary[0]) if coordinate > self.boundary[1]: - return {'position': self._end, 'offset': coordinate - self.boundary[1]} - return {'position': coordinate - self.boundary[0], 'offset': 0} + return Point(position=self._end, offset=coordinate - self.boundary[1]) + return Point(position=coordinate - self.boundary[0], offset=0) - def to_coordinate(self, point: dict[str, int]) -> int: + def to_coordinate(self, point: Point) -> int: """Convert a point model to a coordinate. - :arg dict point: Point model with 'position' and 'offset' keys. + :arg Point point: Point model with 'position' and 'offset'. :returns int: Coordinate. """ if self._inverted: - return self.boundary[1] - point['position'] - point['offset'] - return self.boundary[0] + point['position'] + point['offset'] + return self.boundary[1] - point.position - point.offset + return self.boundary[0] + point.position + point.offset diff --git a/mutalyzer_crossmapper/models.py b/mutalyzer_crossmapper/models.py index 0208e8a..c2d6635 100644 --- a/mutalyzer_crossmapper/models.py +++ b/mutalyzer_crossmapper/models.py @@ -3,8 +3,11 @@ from dataclasses import asdict, dataclass from typing import Any -NONCODING_REGIONS = {'', 'u', 'd'} -CODING_REGIONS = NONCODING_REGIONS | {'-', '*'} +@dataclass(slots=True) +class Point: + position: int + offset: int = 0 + region: str = "" @dataclass(slots=True) @@ -16,18 +19,20 @@ def __post_init__(self) -> None: @staticmethod def _validate_position(position: int) -> None: - if not isinstance(position, int) or position < 0: + if not isinstance(position, int): raise TypeError("position must be a non-negative integer") + if position < 0: + raise ValueError("position must be a non-negative integer") def to_dict(self) -> dict[str, Any]: - return {'position': self.position} + return asdict(self) @classmethod def to_dataclass(cls, point: Any) -> GenomicPoint: if isinstance(point, cls): return point - if isinstance(point, dict): - return cls(position=point["position"]) + if isinstance(point, Point): + return cls(position=point.position) raise TypeError(f"Cannot convert {type(point)}") @@ -36,7 +41,7 @@ class NonCodingPoint(GenomicPoint): offset: int = 0 region: str = "" - allowed_regions = NONCODING_REGIONS + allowed_regions = {'', 'u', 'd'} def __post_init__(self) -> None: GenomicPoint.__post_init__(self) @@ -49,55 +54,46 @@ def _validate_offset(offset: int) -> None: raise TypeError("offset must be an integer") def _validate_region(self, region: str) -> None: - if not isinstance(region, str) or region not in self.allowed_regions: + if not isinstance(region, str): + raise TypeError(f"region must be a string in {self.allowed_regions}") + if region not in self.allowed_regions: raise ValueError(f"region must be a string in {self.allowed_regions}") - def to_dict(self) -> dict[str, Any]: - return { - 'position': self.position, - 'offset': self.offset, - 'region': self.region, - } - @classmethod def to_dataclass(cls, point: Any) -> NonCodingPoint: if isinstance(point, cls): return point + if isinstance(point, Point): + return cls(position=point.position, offset=point.offset, region=point.region) + if isinstance(point, GenomicPoint): return cls(position=point.position) - if isinstance(point, dict): - return cls( - position=point["position"], - offset=point.get("offset", 0), - region=point.get("region", ""), - ) - raise TypeError(f"Cannot convert {type(point)}") @dataclass(slots=True) class CodingPoint(NonCodingPoint): - allowed_regions = CODING_REGIONS + allowed_regions = {'', 'u', 'd', '-', '*'} @classmethod def to_dataclass(cls, point: Any) -> CodingPoint: if isinstance(point, cls): return point - if isinstance(point, NonCodingPoint): + if isinstance(point, Point): return cls( position=point.position, offset=point.offset, region=point.region, ) - if isinstance(point, dict): + if isinstance(point, NonCodingPoint): return cls( - position=point["position"], - offset=point.get("offset", 0), - region=point.get("region", ""), + position=point.position, + offset=point.offset, + region=point.region, ) raise TypeError(f"Cannot convert {type(point)}") @@ -111,14 +107,6 @@ def __post_init__(self) -> None: CodingPoint.__post_init__(self) self._validate_position_in_codon(self.position_in_codon) - def to_dict(self) -> dict[str, Any]: - return { - 'position': self.position, - 'offset': self.offset, - 'region': self.region, - 'position_in_codon': self.position_in_codon, - } - @staticmethod def _validate_position_in_codon(position_in_codon: int) -> None: if not isinstance(position_in_codon, int) or position_in_codon not in (1, 2, 3): @@ -129,7 +117,7 @@ def to_dataclass(cls, point: Any) -> ProteinPoint: if isinstance(point, cls): return point - if isinstance(point, CodingPoint): + if isinstance(point, Point): return cls( position=point.position, offset=point.offset, @@ -137,12 +125,12 @@ def to_dataclass(cls, point: Any) -> ProteinPoint: position_in_codon=getattr(point, "position_in_codon", 1), ) - if isinstance(point, dict): + if isinstance(point, CodingPoint): return cls( - position=point["position"], - offset=point.get("offset", 0), - region=point.get("region", ""), - position_in_codon=point.get("position_in_codon", 1), + position=point.position, + offset=point.offset, + region=point.region, + position_in_codon=getattr(point, "position_in_codon", 1), ) raise TypeError(f"Cannot convert {type(point)}") diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index b9534ae..e1da3bc 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -3,6 +3,7 @@ from .location import nearest_location from .locus import Locus +from .models import Point def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: @@ -49,49 +50,52 @@ def outside(self, coordinate: int) -> int: return coordinate - self._loci[-1].boundary[1] return 0 - def to_position(self, coordinate: int) -> dict[str, int | str]: + def to_position(self, coordinate: int) -> Point: """Convert a coordinate to a point model. :arg int coordinate: Coordinate. - :returns dict: Point model 'position', 'offset' and 'region' keys. + :returns Point: Point model with 'position', 'offset', and 'region'. """ index = nearest_location(self._locations, coordinate, self._inverted) outside = self._orientation * self.outside(coordinate) region = 'u' if outside < 0 else 'd' if outside > 0 else '' point = self._loci[index].to_position(coordinate) - return { - 'position': point['position'] + self._offsets[self._direction(index)], - 'offset': point['offset'], - 'region': region, - } + return Point( + position=point.position + self._offsets[self._direction(index)], + offset=point.offset, + region=region, + ) - def to_coordinate(self, point: dict[str, int | str]) -> int: + def to_coordinate(self, point: Point) -> int: """Convert a point model to a coordinate. - :arg dict point: Point model with 'position','offset' and 'region' keys. + :arg Point point: Point model with 'position', 'offset', and 'region'. :returns int: Coordinate. """ - region = point['region'] + if not isinstance(point, Point): + raise TypeError(f"Cannot convert {type(point)}") + + region = point.region if region == 'u': if self._inverted: - return self._locations[-1][1] - point['offset'] - 1 - return self._locations[0][0] + point['offset'] + return self._locations[-1][1] - point.offset - 1 + return self._locations[0][0] + point.offset if region == 'd': if self._inverted: - return self._locations[0][0] - point['offset'] - return self._locations[-1][1] + point['offset'] - 1 + return self._locations[0][0] - point.offset + return self._locations[-1][1] + point.offset - 1 index = min( len(self._offsets), - max(0, bisect_right(self._offsets, point['position']) - 1) + max(0, bisect_right(self._offsets, point.position) - 1) ) return self._loci[self._direction(index)].to_coordinate( - { - 'position': point['position'] - self._offsets[index], - 'offset': point['offset'], - 'region': point['region'], - } + Point( + position=point.position - self._offsets[index], + offset=point.offset, + region=point.region, + ) ) From cfb910702ac2517499b8fd028ca162f4bdeeccde Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 19 Jun 2026 11:57:37 +0200 Subject: [PATCH 149/236] Update error raise, add dataclass serialization and use single quate. --- mutalyzer_crossmapper/models.py | 98 ++++++--------------------------- 1 file changed, 17 insertions(+), 81 deletions(-) diff --git a/mutalyzer_crossmapper/models.py b/mutalyzer_crossmapper/models.py index c2d6635..8704dcf 100644 --- a/mutalyzer_crossmapper/models.py +++ b/mutalyzer_crossmapper/models.py @@ -3,11 +3,13 @@ from dataclasses import asdict, dataclass from typing import Any + +# Basic dataclass module for locus and multi_locus @dataclass(slots=True) class Point: position: int offset: int = 0 - region: str = "" + region: str = '' @dataclass(slots=True) @@ -17,29 +19,19 @@ class GenomicPoint: def __post_init__(self) -> None: self._validate_position(self.position) + def __str__(self) -> str: + return f"{self.position}" + @staticmethod def _validate_position(position: int) -> None: - if not isinstance(position, int): - raise TypeError("position must be a non-negative integer") - if position < 0: - raise ValueError("position must be a non-negative integer") - - def to_dict(self) -> dict[str, Any]: - return asdict(self) - - @classmethod - def to_dataclass(cls, point: Any) -> GenomicPoint: - if isinstance(point, cls): - return point - if isinstance(point, Point): - return cls(position=point.position) - raise TypeError(f"Cannot convert {type(point)}") + if not isinstance(position, int) or position < 0: + raise ValueError('position must be a non-negative integer') @dataclass(slots=True) class NonCodingPoint(GenomicPoint): offset: int = 0 - region: str = "" + region: str = '' allowed_regions = {'', 'u', 'd'} @@ -51,53 +43,22 @@ def __post_init__(self) -> None: @staticmethod def _validate_offset(offset: int) -> None: if not isinstance(offset, int): - raise TypeError("offset must be an integer") + raise TypeError('offset must be an integer') def _validate_region(self, region: str) -> None: - if not isinstance(region, str): - raise TypeError(f"region must be a string in {self.allowed_regions}") - if region not in self.allowed_regions: - raise ValueError(f"region must be a string in {self.allowed_regions}") - - @classmethod - def to_dataclass(cls, point: Any) -> NonCodingPoint: - if isinstance(point, cls): - return point + if not isinstance(region, str) or region not in self.allowed_regions: + raise ValueError(f'region must be a string in {self.allowed_regions}') - if isinstance(point, Point): - return cls(position=point.position, offset=point.offset, region=point.region) - - if isinstance(point, GenomicPoint): - return cls(position=point.position) - - raise TypeError(f"Cannot convert {type(point)}") + def __str__(self) -> str: + if self.offset == 0: + return f"{self.region}{self.position}" + return f"{self.region}{self.position}{self.offset:+}" @dataclass(slots=True) class CodingPoint(NonCodingPoint): allowed_regions = {'', 'u', 'd', '-', '*'} - @classmethod - def to_dataclass(cls, point: Any) -> CodingPoint: - if isinstance(point, cls): - return point - - if isinstance(point, Point): - return cls( - position=point.position, - offset=point.offset, - region=point.region, - ) - - if isinstance(point, NonCodingPoint): - return cls( - position=point.position, - offset=point.offset, - region=point.region, - ) - - raise TypeError(f"Cannot convert {type(point)}") - @dataclass(slots=True) class ProteinPoint(CodingPoint): @@ -110,29 +71,4 @@ def __post_init__(self) -> None: @staticmethod def _validate_position_in_codon(position_in_codon: int) -> None: if not isinstance(position_in_codon, int) or position_in_codon not in (1, 2, 3): - raise ValueError("position_in_codon must be 1, 2, or 3") - - @classmethod - def to_dataclass(cls, point: Any) -> ProteinPoint: - if isinstance(point, cls): - return point - - if isinstance(point, Point): - return cls( - position=point.position, - offset=point.offset, - region=point.region, - position_in_codon=getattr(point, "position_in_codon", 1), - ) - - if isinstance(point, CodingPoint): - return cls( - position=point.position, - offset=point.offset, - region=point.region, - position_in_codon=getattr(point, "position_in_codon", 1), - ) - - raise TypeError(f"Cannot convert {type(point)}") - - + raise ValueError('position_in_codon must be 1, 2, or 3') From dc93f87f1d104df6f3ea76cbac3f3405ca03973e Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 19 Jun 2026 12:03:26 +0200 Subject: [PATCH 150/236] Update to use dataclass, add a return in Coding class to avoid intermediate negative position value in degenerate. --- mutalyzer_crossmapper/crossmapper.py | 50 +++++++++++++--------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index b8d1ce2..632dde0 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -17,11 +17,11 @@ def coordinate_to_genomic(self, coordinate: int) -> GenomicPoint: def genomic_to_coordinate(self, point: GenomicPoint) -> int: """Convert a genomic point (g./m./o.) to a coordinate. - :arg dict point: Genomic point model. + :arg GenomicPoint point: Genomic point model. :returns int: Coordinate. """ - return GenomicPoint.to_dataclass(point).position - 1 + return point.position - 1 class NonCoding(Genomic): @@ -43,7 +43,7 @@ def coordinate_to_noncoding(self, coordinate: int) -> NonCodingPoint: :returns NonCodingPoint: Noncoding point model. """ - point = NonCodingPoint.to_dataclass(self._noncoding.to_position(coordinate)) + point = self._noncoding.to_position(coordinate) return NonCodingPoint( position=point.position + 1, offset=point.offset, @@ -57,12 +57,11 @@ def noncoding_to_coordinate(self, point: NonCodingPoint) -> int: :returns int: Coordinate. """ - noncoding_point = NonCodingPoint.to_dataclass(point) return self._noncoding.to_coordinate( NonCodingPoint( - position=noncoding_point.position - 1, - offset=noncoding_point.offset, - region=noncoding_point.region, + position=point.position - 1, + offset=point.offset, + region=point.region, ) ) @@ -111,9 +110,9 @@ def _coordinate_to_coding(self, coordinate: int) -> CodingPoint: :arg int coordinate: Coordinate. - :returns dict: Coding position model (c./r.). + :returns CodingPoint: Coding position model (c./r.). """ - noncoding_point = NonCodingPoint.to_dataclass(self._noncoding.to_position(coordinate)) + noncoding_point = self._noncoding.to_position(coordinate) position = noncoding_point.position offset = noncoding_point.offset @@ -176,35 +175,33 @@ def coding_to_coordinate(self, point: CodingPoint) -> int: :returns int: Coordinate. """ - coding_point = CodingPoint.to_dataclass(point) - region = coding_point.region + region = point.region if region in ('u', 'd'): - return self._noncoding.to_coordinate(coding_point) + return self._noncoding.to_coordinate(point) - position = coding_point.position + position = point.position if region == '': noncoding_point = NonCodingPoint( position=position + self._coding[0] - 1, - offset=coding_point.offset, + offset=point.offset, region='', ) return self._noncoding.to_coordinate(noncoding_point) if region == '-': - print(self._coding, position) if position <= self._coding[0]: return self._noncoding.to_coordinate( NonCodingPoint( position=self._coding[0] - position, - offset=coding_point.offset, + offset=point.offset, region='', ) ) return self._noncoding.to_coordinate( NonCodingPoint( position=0, - offset=coding_point.offset + 1 - position, + offset=point.offset + 1 - position, region='u', ) ) @@ -212,7 +209,7 @@ def coding_to_coordinate(self, point: CodingPoint) -> int: return self._noncoding.to_coordinate( NonCodingPoint( position=self._coding[1] + position - 1, - offset=coding_point.offset, + offset=point.offset, region='', ) ) @@ -224,7 +221,7 @@ def coordinate_to_protein(self, coordinate: int) -> ProteinPoint: :returns ProteinPoint: Protein position model(p.). """ - point = CodingPoint.to_dataclass(self.coordinate_to_coding(coordinate)) + point = self.coordinate_to_coding(coordinate) position = point.position if point.region in ('-', 'u'): @@ -248,19 +245,18 @@ def protein_to_coordinate(self, point: ProteinPoint) -> int: :returns int: Coordinate. """ - protein_point = ProteinPoint.to_dataclass(point) - if protein_point.region in ('-', 'u'): + if point.region in ('-', 'u'): return self.coding_to_coordinate( CodingPoint( - position=3 * protein_point.position - protein_point.position_in_codon + 1, - offset=protein_point.offset, - region=protein_point.region, + position=3 * point.position - point.position_in_codon + 1, + offset=point.offset, + region=point.region, ) ) return self.coding_to_coordinate( CodingPoint( - position=3 * protein_point.position + protein_point.position_in_codon - 3, - offset=protein_point.offset, - region=protein_point.region, + position=3 * point.position + point.position_in_codon - 3, + offset=point.offset, + region=point.region, ) ) From 5cc99a87c9416e085346863d118413cb62144f0f Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 19 Jun 2026 17:21:50 +0200 Subject: [PATCH 151/236] Use dataclass point as the basic unit. --- mutalyzer_crossmapper/multi_locus.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index e1da3bc..13c1bd3 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -55,7 +55,7 @@ def to_position(self, coordinate: int) -> Point: :arg int coordinate: Coordinate. - :returns Point: Point model with 'position', 'offset', and 'region'. + :returns Point: CodingPoint model with 'position', 'offset', and 'region'. """ index = nearest_location(self._locations, coordinate, self._inverted) outside = self._orientation * self.outside(coordinate) @@ -70,12 +70,11 @@ def to_position(self, coordinate: int) -> Point: def to_coordinate(self, point: Point) -> int: """Convert a point model to a coordinate. - :arg Point point: Point model with 'position', 'offset', and 'region'. + :arg CodingPoint point: Point model with 'position', 'offset', and 'region'. :returns int: Coordinate. """ - if not isinstance(point, Point): - raise TypeError(f"Cannot convert {type(point)}") + region = point.region From 4be2bc3c091b2ac7a1071893696c5328e3b83b25 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 19 Jun 2026 17:22:35 +0200 Subject: [PATCH 152/236] Update tests to use dataclass. --- tests/test_crossmapper.py | 509 +++++++++++++------------------------- tests/test_locus.py | 33 +-- tests/test_multi_locus.py | 87 +++---- 3 files changed, 230 insertions(+), 399 deletions(-) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index fc09c1a..2b6a92d 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -1,4 +1,5 @@ from mutalyzer_crossmapper import Coding, Genomic, NonCoding +from mutalyzer_crossmapper.models import CodingPoint, GenomicPoint, NonCodingPoint, ProteinPoint from helper import degenerate_equal, invariant @@ -14,13 +15,13 @@ def test_Genomic(): crossmap.coordinate_to_genomic, 0, crossmap.genomic_to_coordinate, - {'position': 1}, + GenomicPoint(position=1), ) invariant( crossmap.coordinate_to_genomic, 98, crossmap.genomic_to_coordinate, - {'position': 99}, + GenomicPoint(position=99), ) @@ -33,19 +34,19 @@ def test_NonCoding(): crossmap.coordinate_to_noncoding, 3, crossmap.noncoding_to_coordinate, - {'position': 1, 'offset': -2, 'region': 'u'}, + NonCodingPoint(position=1, offset=-2, region='u'), ) invariant( crossmap.coordinate_to_noncoding, 4, crossmap.noncoding_to_coordinate, - {'position': 1, 'offset': -1, 'region': 'u'}, + NonCodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_noncoding, 5, crossmap.noncoding_to_coordinate, - {'position': 1, 'offset': 0, 'region': ''}, + NonCodingPoint(position=1, offset=0, region=''), ) # Boundary between downstream and transcript. @@ -53,13 +54,13 @@ def test_NonCoding(): crossmap.coordinate_to_noncoding, 71, crossmap.noncoding_to_coordinate, - {'position': 22, 'offset': 0, 'region': ''}, + NonCodingPoint(position=22, offset=0, region=''), ) invariant( crossmap.coordinate_to_noncoding, 72, crossmap.noncoding_to_coordinate, - {'position': 22, 'offset': 1, 'region': 'd'}, + NonCodingPoint(position=22, offset=1, region='d'), ) @@ -72,13 +73,13 @@ def test_NonCoding_inverted(): crossmap.coordinate_to_noncoding, 72, crossmap.noncoding_to_coordinate, - {'position': 1, 'offset': -1, 'region': 'u'}, + NonCodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_noncoding, 71, crossmap.noncoding_to_coordinate, - {'position': 1, 'offset': 0, 'region': ''}, + NonCodingPoint(position=1, offset=0, region=''), ) # Boundary between downstream and transcript. @@ -86,13 +87,13 @@ def test_NonCoding_inverted(): crossmap.coordinate_to_noncoding, 5, crossmap.noncoding_to_coordinate, - {'position': 22, 'offset': 0, 'region': ''}, + NonCodingPoint(position=22, offset=0, region=''), ) invariant( crossmap.coordinate_to_noncoding, 4, crossmap.noncoding_to_coordinate, - {'position': 22, 'offset': 1, 'region': 'd'}, + NonCodingPoint(position=22, offset=1, region='d'), ) @@ -105,8 +106,8 @@ def test_NonCoding_degenerate(): crossmap.noncoding_to_coordinate, 4, [ - {'position': 1, 'offset': -1, 'region': ''}, - {'position': 1, 'offset': -1, 'region': 'u'}, + NonCodingPoint(position=1, offset=-1, region=''), + NonCodingPoint(position=1, offset=-1, region='u'), ], ) @@ -115,10 +116,10 @@ def test_NonCoding_degenerate(): crossmap.noncoding_to_coordinate, 72, [ - {'position': 22, 'offset': 1, 'region': 'd'}, - {'position': 22, 'offset': 1, 'region': ''}, - {'position': 23, 'offset': 0, 'region': ''}, - {'position': 24, 'offset': -1, 'region': ''}, + NonCodingPoint(position=22, offset=1, region='d'), + NonCodingPoint(position=22, offset=1, region=''), + NonCodingPoint(position=23, offset=0, region=''), + NonCodingPoint(position=24, offset=-1, region=''), ], ) @@ -132,8 +133,8 @@ def test_NonCoding_inverted_degenerate(): crossmap.noncoding_to_coordinate, 72, [ - {'position': 1, 'offset': -1, 'region': ''}, - {'position': 1, 'offset': -1, 'region': 'u'}, + NonCodingPoint(position=1, offset=-1, region=''), + NonCodingPoint(position=1, offset=-1, region='u'), ], ) @@ -142,9 +143,9 @@ def test_NonCoding_inverted_degenerate(): crossmap.noncoding_to_coordinate, 4, [ - {'position': 22, 'offset': 1, 'region': 'd'}, - {'position': 23, 'offset': 0, 'region': ''}, - {'position': 22, 'offset': 1, 'region': ''}, + NonCodingPoint(position=22, offset=1, region='d'), + NonCodingPoint(position=23, offset=0, region=''), + NonCodingPoint(position=22, offset=1, region=''), ], ) @@ -158,13 +159,13 @@ def test_Coding(): crossmap.coordinate_to_coding, 31, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': '-'}, + CodingPoint(position=1, offset=0, region='-'), ) invariant( crossmap.coordinate_to_coding, 32, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': ''}, + CodingPoint(position=1, offset=0, region=''), ) # Boundary between CDS and 3'. @@ -172,13 +173,13 @@ def test_Coding(): crossmap.coordinate_to_coding, 42, crossmap.coding_to_coordinate, - {'position': 6, 'offset': 0, 'region': ''}, + CodingPoint(position=6, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, 43, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': '*'}, + CodingPoint(position=1, offset=0, region='*'), ) @@ -191,13 +192,13 @@ def test_Coding_inverted(): crossmap.coordinate_to_coding, 43, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': '-'}, + CodingPoint(position=1, offset=0, region='-'), ) invariant( crossmap.coordinate_to_coding, 42, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': ''}, + CodingPoint(position=1, offset=0, region=''), ) # Boundary between CDS and 3'. @@ -205,13 +206,13 @@ def test_Coding_inverted(): crossmap.coordinate_to_coding, 32, crossmap.coding_to_coordinate, - {'position': 6, 'offset': 0, 'region': ''}, + CodingPoint(position=6, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, 31, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': '*'}, + CodingPoint(position=1, offset=0, region='*'), ) @@ -224,13 +225,13 @@ def test_Coding_regions(): crossmap.coordinate_to_coding, 25, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 5, 'region': '-'}, + CodingPoint(position=1, offset=5, region='-'), ) invariant( crossmap.coordinate_to_coding, 26, crossmap.coding_to_coordinate, - {'position': 1, 'offset': -4, 'region': ''}, + CodingPoint(position=1, offset=-4, region=''), ) # Downstream odd length intron between two regions. @@ -238,13 +239,13 @@ def test_Coding_regions(): crossmap.coordinate_to_coding, 44, crossmap.coding_to_coordinate, - {'position': 10, 'offset': 5, 'region': ''}, + CodingPoint(position=10, offset=5, region=''), ) invariant( crossmap.coordinate_to_coding, 45, crossmap.coding_to_coordinate, - {'position': 1, 'offset': -4, 'region': '*'}, + CodingPoint(position=1, offset=-4, region='*'), ) @@ -257,13 +258,13 @@ def test_Coding_regions_inverted(): crossmap.coordinate_to_coding, 44, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 5, 'region': '-'}, + CodingPoint(position=1, offset=5, region='-'), ) invariant( crossmap.coordinate_to_coding, 43, crossmap.coding_to_coordinate, - {'position': 1, 'offset': -4, 'region': ''}, + CodingPoint(position=1, offset=-4, region=''), ) # Downstream odd length intron between two regions. @@ -271,13 +272,13 @@ def test_Coding_regions_inverted(): crossmap.coordinate_to_coding, 25, crossmap.coding_to_coordinate, - {'position': 10, 'offset': 5, 'region': ''}, + CodingPoint(position=10, offset=5, region=''), ) invariant( crossmap.coordinate_to_coding, 24, crossmap.coding_to_coordinate, - {'position': 1, 'offset': -4, 'region': '*'}, + CodingPoint(position=1, offset=-4, region='*'), ) @@ -290,13 +291,13 @@ def test_Coding_no_utr5(): crossmap.coordinate_to_coding, 9, crossmap.coding_to_coordinate, - {'position': 1, 'offset': -1, 'region': 'u'}, + CodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_coding, 10, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': ''}, + CodingPoint(position=1, offset=0, region=''), ) @@ -307,7 +308,7 @@ def test_Coding_no_intron(): crossmap.coordinate_to_coding, 20, crossmap.coding_to_coordinate, - {'position': 6, 'offset': 0, 'region': ''}, + CodingPoint(position=6, offset=0, region=''), ) @@ -318,7 +319,7 @@ def test_Coding_no_intron_inverted(): crossmap.coordinate_to_coding, 20, crossmap.coding_to_coordinate, - {'position': 5, 'offset': 0, 'region': ''}, + CodingPoint(position=5, offset=0, region=''), ) @@ -329,7 +330,7 @@ def test_Coding_one_base_intron(): crossmap.coordinate_to_coding, 19, crossmap.coding_to_coordinate, - {'position': 4, 'offset': 1, 'region': ''}, + CodingPoint(position=4, offset=1, region=''), ) @@ -340,7 +341,7 @@ def test_Coding_one_base_intron_inverted(): crossmap.coordinate_to_coding, 19, crossmap.coding_to_coordinate, - {'position': 5, 'offset': 1, 'region': ''}, + CodingPoint(position=5, offset=1, region=''), ) @@ -353,13 +354,13 @@ def test_Coding_no_utr5_inverted(): crossmap.coordinate_to_coding, 20, crossmap.coding_to_coordinate, - {'position': 1, 'offset': -1, 'region': 'u'}, + CodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_coding, 19, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': ''}, + CodingPoint(position=1, offset=0, region=''), ) @@ -372,13 +373,13 @@ def test_Coding_no_utr3(): crossmap.coordinate_to_coding, 19, crossmap.coding_to_coordinate, - {'position': 5, 'offset': 0, 'region': ''}, + CodingPoint(position=5, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, 20, crossmap.coding_to_coordinate, - {'position': 5, 'offset': 1, 'region': 'd'}, + CodingPoint(position=5, offset=1, region='d'), ) @@ -391,13 +392,13 @@ def test_Coding_no_utr3_inverted(): crossmap.coordinate_to_coding, 10, crossmap.coding_to_coordinate, - {'position': 5, 'offset': 0, 'region': ''}, + CodingPoint(position=5, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, 9, crossmap.coding_to_coordinate, - {'position': 5, 'offset': 1, 'region': 'd'}, + CodingPoint(position=5, offset=1, region='d'), ) @@ -410,19 +411,19 @@ def test_Coding_small_utr5(): crossmap.coordinate_to_coding, 9, crossmap.coding_to_coordinate, - {'position': 1, 'offset': -1, 'region': 'u'}, + CodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_coding, 10, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': '-'}, + CodingPoint(position=1, offset=0, region='-'), ) invariant( crossmap.coordinate_to_coding, 11, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': ''}, + CodingPoint(position=1, offset=0, region=''), ) @@ -435,19 +436,19 @@ def test_Coding_small_utr5_inverted(): crossmap.coordinate_to_coding, 20, crossmap.coding_to_coordinate, - {'position': 1, 'offset': -1, 'region': 'u'}, + CodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_coding, 19, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': '-'}, + CodingPoint(position=1, offset=0, region='-'), ) invariant( crossmap.coordinate_to_coding, 18, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': ''}, + CodingPoint(position=1, offset=0, region=''), ) @@ -460,19 +461,19 @@ def test_Coding_small_utr3(): crossmap.coordinate_to_coding, 18, crossmap.coding_to_coordinate, - {'position': 4, 'offset': 0, 'region': ''}, + CodingPoint(position=4, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, 19, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': '*'}, + CodingPoint(position=1, offset=0, region='*'), ) invariant( crossmap.coordinate_to_coding, 20, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 1, 'region': 'd'}, + CodingPoint(position=1, offset=1, region='d'), ) @@ -485,19 +486,19 @@ def test_Coding_small_utr3_inverted(): crossmap.coordinate_to_coding, 11, crossmap.coding_to_coordinate, - {'position': 4, 'offset': 0, 'region': ''}, + CodingPoint(position=4, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, 10, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 0, 'region': '*'}, + CodingPoint(position=1, offset=0, region='*'), ) invariant( crossmap.coordinate_to_coding, 9, crossmap.coding_to_coordinate, - {'position': 1, 'offset': 1, 'region': 'd'}, + CodingPoint(position=1, offset=1, region='d'), ) @@ -510,25 +511,25 @@ def test_Coding_degenerate(): crossmap.coding_to_coordinate, 9, [ - {'position': 1, 'offset': -1, 'region': 'u'}, - {'position': 2, 'offset': 0, 'region': '-'}, - {'position': 1, 'offset': -2, 'region': ''}, - {'position': 1, 'offset': -10, 'region': '*'}, - {'position': 2, 'offset': -11, 'region': '*'}, - {'position': 3, 'offset': 1, 'region': '-'}, - {'position': 4, 'offset': 2, 'region': '-'}, + CodingPoint(position=1, offset=-1, region='u'), + CodingPoint(position=2, offset=0, region='-'), + CodingPoint(position=1, offset=-2, region=''), + CodingPoint(position=1, offset=-10, region='*'), + CodingPoint(position=2, offset=-11, region='*'), + CodingPoint(position=3, offset=1, region='-'), + CodingPoint(position=4, offset=2, region='-'), ], ) degenerate_equal( crossmap.coding_to_coordinate, 20, [ - {'position': 9, 'offset': 1, 'region': 'd'}, - {'position': 2, 'offset': 0, 'region': '*'}, - {'position': 8, 'offset': 2, 'region': ''}, - {'position': 1, 'offset': 10, 'region': '-'}, - {'position': 2, 'offset': 11, 'region': '-'}, - {'position': 7, 'offset': 3, 'region': ''}, + CodingPoint(position=9, offset=1, region='d'), + CodingPoint(position=2, offset=0, region='*'), + CodingPoint(position=8, offset=2, region=''), + CodingPoint(position=1, offset=10, region='-'), + CodingPoint(position=2, offset=11, region='-'), + CodingPoint(position=7, offset=3, region=''), ], ) @@ -541,23 +542,23 @@ def test_Coding_inverted_degenerate(): crossmap.coding_to_coordinate, 20, [ - {'position': 1, 'offset': -1, 'region': 'u'}, - {'position': 2, 'offset': 0, 'region': '-'}, - {'position': 1, 'offset': -2, 'region': ''}, - {'position': 1, 'offset': -10, 'region': '*'}, - {'position': 1, 'offset': -10, 'region': 'd'}, - {'position': 2, 'offset': -3, 'region': ''}, + CodingPoint(position=1, offset=-1, region='u'), + CodingPoint(position=2, offset=0, region='-'), + CodingPoint(position=1, offset=-2, region=''), + CodingPoint(position=1, offset=-10, region='*'), + CodingPoint(position=1, offset=-10, region='d'), + CodingPoint(position=2, offset=-3, region=''), ], ) degenerate_equal( crossmap.coding_to_coordinate, 9, [ - {'position': 2, 'offset': 1, 'region': 'd'}, - {'position': 2, 'offset': 0, 'region': '*'}, - {'position': 8, 'offset': 2, 'region': ''}, - {'position': 1, 'offset': 10, 'region': '-'}, - {'position': 1, 'offset': 10, 'region': 'u'}, + CodingPoint(position=2, offset=1, region='d'), + CodingPoint(position=2, offset=0, region='*'), + CodingPoint(position=8, offset=2, region=''), + CodingPoint(position=1, offset=10, region='-'), + CodingPoint(position=1, offset=10, region='u'), ], ) @@ -566,251 +567,103 @@ def test_Coding_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" crossmap = Coding([(10, 20)], (11, 19)) - assert crossmap.coordinate_to_coding(9, True) == { - 'position': 2, - 'offset': 0, - 'region': '-', - } - assert crossmap.coordinate_to_coding(20, True) == { - 'position': 2, - 'offset': 0, - 'region': '*', - } + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=2, offset=0, region='-') + assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=2, offset=0, region='*') def test_Coding_inverted_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" crossmap = Coding([(10, 20)], (11, 19), True) - assert crossmap.coordinate_to_coding(20, True) == { - 'position': 2, - 'offset': 0, - 'region': '-', - } - assert crossmap.coordinate_to_coding(25, True) == { - 'position': 7, - 'offset': 0, - 'region': '-', - } - assert crossmap.coordinate_to_coding(9, True) == { - 'position': 2, - 'offset': 0, - 'region': '*', - } + assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=2, offset=0, region='-') + assert crossmap.coordinate_to_coding(25, True) == CodingPoint(position=7, offset=0, region='-') + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=2, offset=0, region='*') def test_Coding_no_utr5_degenerate_return(): """A 5' UTR may be missing.""" crossmap = Coding([(10, 20)], (10, 15)) - assert crossmap.coordinate_to_coding(9, True) == { - 'position': 1, - 'offset': 0, - 'region': '-' - } - assert crossmap.coordinate_to_coding(10, True) == { - 'position': 1, - 'offset': 0, - 'region': '' - } - assert crossmap.coordinate_to_coding(19, True) == { - 'position': 5, - 'offset': 0, - 'region': '*' - } - assert crossmap.coordinate_to_coding(20, True) == { - 'position': 6, - 'offset': 0, - 'region': '*' - } + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=1, offset=0, region='-') + assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=1, offset=0, region='') + assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=5, offset=0, region='*') + assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=6, offset=0, region='*') def test_Coding_no_utr5_inverted_degenerate_return(): """A 5' UTR may be missing.""" crossmap = Coding([(10, 20)], (10, 15), True) - assert crossmap.coordinate_to_coding(9, True) == { - 'position': 1, - 'offset': 0, - 'region': '*' - } - assert crossmap.coordinate_to_coding(10, True) == { - 'position': 5, - 'offset': 0, - 'region': '' - } - assert crossmap.coordinate_to_coding(19, True) == { - 'position': 5, - 'offset': 0, - 'region': '-' - } - assert crossmap.coordinate_to_coding(20, True) == { - 'position': 6, - 'offset': 0, - 'region': '-' - } + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=1, offset=0, region='*') + assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=5, offset=0, region='') + assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=5, offset=0, region='-') + assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=6, offset=0, region='-') def test_Coding_no_utr3_degenerate_return(): """A 3' UTR may be missing.""" crossmap = Coding([(10, 20)], (15, 20)) - assert crossmap.coordinate_to_coding(9, True) == { - 'position': 6, - 'offset': 0, - 'region': '-' - } - assert crossmap.coordinate_to_coding(10, True) == { - 'position': 5, - 'offset': 0, - 'region': '-', - } - assert crossmap.coordinate_to_coding(19, True) == { - 'position': 5, - 'offset': 0, - 'region': '', - } - assert crossmap.coordinate_to_coding(20, True) == { - 'position': 1, - 'offset': 0, - 'region': '*', - } + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=6, offset=0, region='-') + assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=5, offset=0, region='-') + assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=5, offset=0, region='') + assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=1, offset=0, region='*') def test_Coding_no_utr3_inverted_degenerate_return(): """A 3' UTR may be missing.""" crossmap = Coding([(10, 20)], (15, 20), True) - assert crossmap.coordinate_to_coding(9, True) == { - 'position': 6, - 'offset': 0, - 'region': '*' - } - assert crossmap.coordinate_to_coding(10, True) == { - 'position': 5, - 'offset': 0, - 'region': '*', - } - assert crossmap.coordinate_to_coding(19, True) == { - 'position': 1, - 'offset': 0, - 'region': '', - } - assert crossmap.coordinate_to_coding(20, True) == { - 'position': 1, - 'offset': 0, - 'region': '-', - } + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=6, offset=0, region='*') + assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=5, offset=0, region='*') + assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=1, offset=0, region='') + assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=1, offset=0, region='-') def test_Coding_small_utr5_degenerate_return(): """A 5' UTR may be of length one.""" crossmap = Coding([(10, 20)], (11, 15)) - assert crossmap.coordinate_to_coding(9, True) == { - 'position': 2, - 'offset': 0, - 'region': '-' - } - assert crossmap.coordinate_to_coding(10, True) == { - 'position': 1, - 'offset': 0, - 'region': '-' - } - assert crossmap.coordinate_to_coding(11, True) == { - 'position': 1, - 'offset': 0, - 'region': '' - } + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=2, offset=0, region='-') + assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=1, offset=0, region='-') + assert crossmap.coordinate_to_coding(11, True) == CodingPoint(position=1, offset=0, region='') def test_Coding_small_utr5_inverted_degenerate_return(): """A 5' UTR may be of length one.""" crossmap = Coding([(10, 20)], (11, 15), True) - assert crossmap.coordinate_to_coding(9, True) == { - 'position': 2, - 'offset': 0, - 'region': '*' - } - assert crossmap.coordinate_to_coding(10, True) == { - 'position': 1, - 'offset': 0, - 'region': '*' - } - assert crossmap.coordinate_to_coding(11, True) == { - 'position': 4, - 'offset': 0, - 'region': '' - } + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=2, offset=0, region='*') + assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=1, offset=0, region='*') + assert crossmap.coordinate_to_coding(11, True) == CodingPoint(position=4, offset=0, region='') def test_Coding_small_utr3_degenerate_return(): """A 3' UTR may be of length one.""" crossmap = Coding([(10, 20)], (15, 19)) - assert crossmap.coordinate_to_coding(18, True) == { - 'position': 4, - 'offset': 0, - 'region': '' - } - assert crossmap.coordinate_to_coding(19, True) == { - 'position': 1, - 'offset': 0, - 'region': '*' - } - assert crossmap.coordinate_to_coding(20, True) == { - 'position': 2, - 'offset': 0, - 'region': '*' - } + assert crossmap.coordinate_to_coding(18, True) == CodingPoint(position=4, offset=0, region='') + assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=1, offset=0, region='*') + assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=2, offset=0, region='*') def test_Coding_small_utr3_inverted_degenerate_return(): """A 3' UTR may be of length one.""" crossmap = Coding([(10, 20)], (15, 19), True) - assert crossmap.coordinate_to_coding(18, True) == { - 'position': 1, - 'offset': 0, - 'region': '' - } - assert crossmap.coordinate_to_coding(19, True) == { - 'position': 1, - 'offset': 0, - 'region': '-' - } - assert crossmap.coordinate_to_coding(20, True) == { - 'position': 2, - 'offset': 0, - 'region': '-' - } + assert crossmap.coordinate_to_coding(18, True) == CodingPoint(position=1, offset=0, region='') + assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=1, offset=0, region='-') + assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=2, offset=0, region='-') def test_Coding_two_exons_inverted_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" crossmap = Coding([(10, 20), (30, 40)], (18, 37), True) - assert crossmap.coordinate_to_coding(5, True) == { - 'position': 13, - 'offset': 0, - 'region': '*', - } - assert crossmap.coordinate_to_coding(25, True) == { - 'position': 7, - 'offset': 5, - 'region': '', - } - assert crossmap.coordinate_to_coding(35, True) == { - 'position': 2, - 'offset': 0, - 'region': '', - } - assert crossmap.coordinate_to_coding(38, True) == { - 'position': 2, - 'offset': 0, - 'region': '-', - } + assert crossmap.coordinate_to_coding(5, True) == CodingPoint(position=13, offset=0, region='*') + assert crossmap.coordinate_to_coding(25, True) == CodingPoint(position=7, offset=5, region='') + assert crossmap.coordinate_to_coding(35, True) == CodingPoint(position=2, offset=0, region='') + assert crossmap.coordinate_to_coding(38, True) == CodingPoint(position=2, offset=0, region='-') def test_Coding_degenerate_no_return(): @@ -835,21 +688,21 @@ def test_Coding_no_utr_degenerate(): crossmap.coding_to_coordinate, 9, [ - {'position': 1, 'offset': -1, 'region': 'u'}, - {'position': 1, 'offset': 0, 'region': '-'}, - {'position': 1, 'offset': -2, 'region': '*'}, - {'position': 1, 'offset': -1, 'region': ''}, - {'position': 1, 'offset': -1, 'region': 'd'}, + CodingPoint(position=1, offset=-1, region='u'), + # CodingPoint(position=1, offset=0, region='-'), + CodingPoint(position=1, offset=-2, region='*'), + CodingPoint(position=1, offset=-1, region=''), + CodingPoint(position=1, offset=-1, region='d'), ], ) degenerate_equal( crossmap.coding_to_coordinate, 11, [ - {'position': 1, 'offset': 1, 'region': 'd'}, - {'position': 1, 'offset': 0, 'region': '*'}, - {'position': 1, 'offset': 2, 'region': '-'}, - {'position': 1, 'offset': 1, 'region': ''}, + CodingPoint(position=1, offset=1, region='d'), + CodingPoint(position=1, offset=0, region='*'), + # CodingPoint(position=1, offset=2, region='-'), + CodingPoint(position=1, offset=1, region=''), ], ) @@ -862,22 +715,22 @@ def test_Coding_inverted_no_utr_degenerate(): crossmap.coding_to_coordinate, 11, [ - {'position': 1, 'offset': -1, 'region': 'u'}, - {'position': 1, 'offset': 0, 'region': '-'}, - {'position': 1, 'offset': -2, 'region': '*'}, - {'position': 1, 'offset': -1, 'region': ''}, - {'position': 1, 'offset': -1, 'region': 'd'}, + CodingPoint(position=1, offset=-1, region='u'), + # CodingPoint(position=1, offset=0, region='-'), + CodingPoint(position=1, offset=-2, region='*'), + CodingPoint(position=1, offset=-1, region=''), + CodingPoint(position=1, offset=-1, region='d'), ], ) degenerate_equal( crossmap.coding_to_coordinate, 9, [ - {'position': 1, 'offset': 1, 'region': 'd'}, - {'position': 1, 'offset': 0, 'region': '*'}, - {'position': 1, 'offset': 2, 'region': '-'}, - {'position': 1, 'offset': 1, 'region': ''}, - {'position': 1, 'offset': 1, 'region': 'u'}, + CodingPoint(position=1, offset=1, region='d'), + CodingPoint(position=1, offset=0, region='*'), + # CodingPoint(position=1, offset=2, region='-'), + CodingPoint(position=1, offset=1, region=''), + CodingPoint(position=1, offset=1, region='u'), ], ) @@ -886,42 +739,18 @@ def test_Coding_no_utr_degenerate_return(): """UTRs may be missing.""" crossmap = Coding([(10, 11)], (10, 11)) - assert crossmap.coordinate_to_coding(8, True) == { - 'position': 2, - 'offset': 0, - 'region': '-', - } - assert crossmap.coordinate_to_coding(9, True) == { - 'position': 1, - 'offset': 0, - 'region': '-', - } - assert crossmap.coordinate_to_coding(11, True) == { - 'position': 1, - 'offset': 0, - 'region': '*', - } - assert crossmap.coordinate_to_coding(12, True) == { - 'position': 2, - 'offset': 0, - 'region': '*', - } + assert crossmap.coordinate_to_coding(8, True) == CodingPoint(position=2, offset=0, region='-') + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=1, offset=0, region='-') + assert crossmap.coordinate_to_coding(11, True) == CodingPoint(position=1, offset=0, region='*') + assert crossmap.coordinate_to_coding(12, True) == CodingPoint(position=2, offset=0, region='*') def test_Coding_inverted_no_utr_degenerate_return(): """UTRs may be missing.""" crossmap = Coding([(10, 11)], (10, 11), True) - assert crossmap.coordinate_to_coding(11, True) == { - 'position': 1, - 'offset': 0, - 'region': '-', - } - assert crossmap.coordinate_to_coding(9, True) == { - 'position': 1, - 'offset': 0, - 'region': '*', - } + assert crossmap.coordinate_to_coding(11, True) == CodingPoint(position=1, offset=0, region='-') + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=1, offset=0, region='*') def test_Coding_protein(): @@ -933,13 +762,13 @@ def test_Coding_protein(): crossmap.coordinate_to_protein, 4, crossmap.protein_to_coordinate, - {'position': 4, 'position_in_codon': 2, 'offset': -1, 'region': 'u'} + ProteinPoint(position=4, offset=-1, region='u', position_in_codon=2) ) invariant( crossmap.coordinate_to_protein, 5, crossmap.protein_to_coordinate, - {'position': 4, 'position_in_codon': 2, 'offset': 0, 'region': '-'} + ProteinPoint(position=4, offset=0, region='-', position_in_codon=2) ) # Boundary between 5' UTR and CDS @@ -947,13 +776,13 @@ def test_Coding_protein(): crossmap.coordinate_to_protein, 31, crossmap.protein_to_coordinate, - {'position': 1, 'position_in_codon': 3, 'offset': 0, 'region': '-'}, + ProteinPoint(position=1, offset=0, region='-', position_in_codon=3), ) invariant( crossmap.coordinate_to_protein, 32, crossmap.protein_to_coordinate, - {'position': 1, 'position_in_codon': 1, 'offset': 0, 'region': ''}, + ProteinPoint(position=1, offset=0, region='', position_in_codon=1), ) # Intron boundary. @@ -961,13 +790,13 @@ def test_Coding_protein(): crossmap.coordinate_to_protein, 34, crossmap.protein_to_coordinate, - {'position': 1, 'position_in_codon': 3, 'offset': 0, 'region': ''}, + ProteinPoint(position=1, offset=0, region='', position_in_codon=3), ) invariant( crossmap.coordinate_to_protein, 35, crossmap.protein_to_coordinate, - {'position': 1, 'position_in_codon': 3, 'offset': 1, 'region': ''}, + ProteinPoint(position=1, offset=1, region='', position_in_codon=3), ) # Boundary between CDS and 3' UTR. @@ -975,13 +804,13 @@ def test_Coding_protein(): crossmap.coordinate_to_protein, 42, crossmap.protein_to_coordinate, - {'position': 2, 'position_in_codon': 3, 'offset': 0, 'region': ''}, + ProteinPoint(position=2, offset=0, region='', position_in_codon=3), ) invariant( crossmap.coordinate_to_protein, 43, crossmap.protein_to_coordinate, - {'position': 1, 'position_in_codon': 1, 'offset': 0, 'region': '*'}, + ProteinPoint(position=1, offset=0, region='*', position_in_codon=1), ) # Boundary between 3' UTR and downstream @@ -989,13 +818,13 @@ def test_Coding_protein(): crossmap.coordinate_to_protein, 71, crossmap.protein_to_coordinate, - {'position': 2, 'position_in_codon': 2, 'offset': 0, 'region': '*'} + ProteinPoint(position=2, offset=0, region='*', position_in_codon=2) ) invariant( crossmap.coordinate_to_protein, 72, crossmap.protein_to_coordinate, - {'position': 2, 'position_in_codon': 2, 'offset': 1, 'region': 'd'} + ProteinPoint(position=2, offset=1, region='d', position_in_codon=2) ) @@ -1008,13 +837,13 @@ def test_Coding_inverted_protein(): crossmap.coordinate_to_protein, 4, crossmap.protein_to_coordinate, - {'position': 4, 'position_in_codon': 2, 'offset': 1, 'region': 'd'} + ProteinPoint(position=4, offset=1, region='d', position_in_codon=2) ) invariant( crossmap.coordinate_to_protein, 5, crossmap.protein_to_coordinate, - {'position': 4, 'position_in_codon': 2, 'offset': 0, 'region': '*'} + ProteinPoint(position=4, offset=0, region='*', position_in_codon=2) ) # Boundary between 5' UTR and CDS @@ -1022,13 +851,13 @@ def test_Coding_inverted_protein(): crossmap.coordinate_to_protein, 31, crossmap.protein_to_coordinate, - {'position': 1, 'position_in_codon': 1, 'offset': 0, 'region': '*'}, + ProteinPoint(position=1, offset=0, region='*', position_in_codon=1), ) invariant( crossmap.coordinate_to_protein, 32, crossmap.protein_to_coordinate, - {'position': 2, 'position_in_codon': 3, 'offset': 0, 'region': ''}, + ProteinPoint(position=2, offset=0, region='', position_in_codon=3), ) # Intron boundary. @@ -1036,13 +865,13 @@ def test_Coding_inverted_protein(): crossmap.coordinate_to_protein, 34, crossmap.protein_to_coordinate, - {'position': 2, 'position_in_codon': 1, 'offset': 0, 'region': ''}, + ProteinPoint(position=2, offset=0, region='', position_in_codon=1), ) invariant( crossmap.coordinate_to_protein, 35, crossmap.protein_to_coordinate, - {'position': 2, 'position_in_codon': 1, 'offset': -1, 'region': ''}, + ProteinPoint(position=2, offset=-1, region='', position_in_codon=1), ) # Boundary between CDS and 3' UTR. @@ -1050,13 +879,13 @@ def test_Coding_inverted_protein(): crossmap.coordinate_to_protein, 42, crossmap.protein_to_coordinate, - {'position': 1, 'position_in_codon': 1, 'offset': 0, 'region': ''}, + ProteinPoint(position=1, offset=0, region='', position_in_codon=1), ) invariant( crossmap.coordinate_to_protein, 43, crossmap.protein_to_coordinate, - {'position': 1, 'position_in_codon': 3, 'offset': 0, 'region': '-'}, + ProteinPoint(position=1, offset=0, region='-', position_in_codon=3), ) # Boundary between 3' UTR and downstream @@ -1064,11 +893,11 @@ def test_Coding_inverted_protein(): crossmap.coordinate_to_protein, 71, crossmap.protein_to_coordinate, - {'position': 2, 'position_in_codon': 2, 'offset': 0, 'region': '-'} + ProteinPoint(position=2, offset=0, region='-', position_in_codon=2) ) invariant( crossmap.coordinate_to_protein, 72, crossmap.protein_to_coordinate, - {'position': 2, 'position_in_codon': 2, 'offset': -1, 'region': 'u'} + ProteinPoint(position=2, offset=-1, region='u', position_in_codon=2) ) diff --git a/tests/test_locus.py b/tests/test_locus.py index 535f93d..2379125 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -1,4 +1,5 @@ from mutalyzer_crossmapper import Locus +from mutalyzer_crossmapper.models import Point from helper import degenerate_equal, invariant @@ -7,37 +8,37 @@ def test_Locus(): """Forward orientent Lovus.""" locus = Locus((30, 35)) - invariant(locus.to_position, 29, locus.to_coordinate, {'position': 0, 'offset': -1}) - invariant(locus.to_position, 30, locus.to_coordinate, {'position': 0, 'offset': 0}) - invariant(locus.to_position, 31, locus.to_coordinate, {'position': 1, 'offset': 0}) - invariant(locus.to_position, 33, locus.to_coordinate, {'position': 3, 'offset': 0}) - invariant(locus.to_position, 34, locus.to_coordinate, {'position': 4, 'offset': 0}) - invariant(locus.to_position, 35, locus.to_coordinate, {'position': 4, 'offset': 1}) + invariant(locus.to_position, 29, locus.to_coordinate, Point(position=0, offset=-1)) + invariant(locus.to_position, 30, locus.to_coordinate, Point(position=0, offset=0)) + invariant(locus.to_position, 31, locus.to_coordinate, Point(position=1, offset=0)) + invariant(locus.to_position, 33, locus.to_coordinate, Point(position=3, offset=0)) + invariant(locus.to_position, 34, locus.to_coordinate, Point(position=4, offset=0)) + invariant(locus.to_position, 35, locus.to_coordinate, Point(position=4, offset=1)) def test_Locus_inverted(): """Reverse orientent Lovus.""" locus = Locus((30, 35), True) - invariant(locus.to_position, 35, locus.to_coordinate, {'position': 0, 'offset': -1}) - invariant(locus.to_position, 34, locus.to_coordinate, {'position': 0, 'offset': 0}) - invariant(locus.to_position, 33, locus.to_coordinate, {'position': 1, 'offset': 0}) - invariant(locus.to_position, 31, locus.to_coordinate, {'position': 3, 'offset': 0}) - invariant(locus.to_position, 30, locus.to_coordinate, {'position': 4, 'offset': 0}) - invariant(locus.to_position, 29, locus.to_coordinate, {'position': 4, 'offset': 1}) + invariant(locus.to_position, 35, locus.to_coordinate, Point(position=0, offset=-1)) + invariant(locus.to_position, 34, locus.to_coordinate, Point(position=0, offset=0)) + invariant(locus.to_position, 33, locus.to_coordinate, Point(position=1, offset=0)) + invariant(locus.to_position, 31, locus.to_coordinate, Point(position=3, offset=0)) + invariant(locus.to_position, 30, locus.to_coordinate, Point(position=4, offset=0)) + invariant(locus.to_position, 29, locus.to_coordinate, Point(position=4, offset=1)) def test_Locus_degenerate(): """Degenerate positions are silently corrected.""" locus = Locus((10, 20)) - degenerate_equal(locus.to_coordinate, 9, [{'position': 0, 'offset': -1}, {'position': -1, 'offset': 0}]) - degenerate_equal(locus.to_coordinate, 20, [{'position': 9, 'offset': 1}, {'position': 10, 'offset': 0}]) + degenerate_equal(locus.to_coordinate, 9, [Point(position=0, offset=-1), Point(position=-1, offset=0)]) + degenerate_equal(locus.to_coordinate, 20, [Point(position=9, offset=1), Point(position=10, offset=0)]) def test_Locus_inverted_degenerate(): """Degenerate positions are silently corrected.""" locus = Locus((10, 20), True) - degenerate_equal(locus.to_coordinate, 20, [{'position': 0, 'offset': -1}, {'position': -1, 'offset': 0}]) - degenerate_equal(locus.to_coordinate, 9, [{'position': 9, 'offset': 1}, {'position': 10, 'offset': 0}]) + degenerate_equal(locus.to_coordinate, 20, [Point(position=0, offset=-1), Point(position=-1, offset=0)]) + degenerate_equal(locus.to_coordinate, 9, [Point(position=9, offset=1), Point(position=10, offset=0)]) diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index 364156c..ce02704 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -1,5 +1,6 @@ from mutalyzer_crossmapper import MultiLocus from mutalyzer_crossmapper.multi_locus import _offsets +from mutalyzer_crossmapper.models import Point from helper import degenerate_equal, invariant @@ -35,14 +36,14 @@ def test_MultiLocus(): multi_locus.to_position, 4, multi_locus.to_coordinate, - {'position': 0, 'offset': -1, 'region': 'u'}, + Point(position=0, offset=-1, region='u'), ) invariant( multi_locus.to_position, 5, multi_locus.to_coordinate, - {'position': 0, 'offset': 0, 'region': ''}, + Point(position=0, offset=0, region=''), ) # Internal locus. @@ -50,37 +51,37 @@ def test_MultiLocus(): multi_locus.to_position, 29, multi_locus.to_coordinate, - {'position': 9, 'offset': -1, 'region': ''}, + Point(position=9, offset=-1, region=''), ) invariant( multi_locus.to_position, 30, multi_locus.to_coordinate, - {'position': 9, 'offset': 0, 'region': ''}, + Point(position=9, offset=0, region=''), ) invariant( multi_locus.to_position, 31, multi_locus.to_coordinate, - {'position': 10, 'offset': 0, 'region': ''}, + Point(position=10, offset=0, region=''), ) invariant( multi_locus.to_position, 33, multi_locus.to_coordinate, - {'position': 12, 'offset': 0, 'region': ''}, + Point(position=12, offset=0, region=''), ) invariant( multi_locus.to_position, 34, multi_locus.to_coordinate, - {'position': 13, 'offset': 0, 'region': ''}, + Point(position=13, offset=0, region=''), ) invariant( multi_locus.to_position, 35, multi_locus.to_coordinate, - {'position': 13, 'offset': 1, 'region': ''}, + Point(position=13, offset=1, region=''), ) # Boundary between the last locus and downstream. @@ -88,13 +89,13 @@ def test_MultiLocus(): multi_locus.to_position, 71, multi_locus.to_coordinate, - {'position': 21, 'offset': 0, 'region': ''}, + Point(position=21, offset=0, region=''), ) invariant( multi_locus.to_position, 72, multi_locus.to_coordinate, - {'position': 21, 'offset': 1, 'region': 'd'}, + Point(position=21, offset=1, region='d'), ) @@ -107,13 +108,13 @@ def test_MultiLocus_inverted(): multi_locus.to_position, 72, multi_locus.to_coordinate, - {'position': 0, 'offset': -1, 'region': 'u'}, + Point(position=0, offset=-1, region='u'), ) invariant( multi_locus.to_position, 71, multi_locus.to_coordinate, - {'position': 0, 'offset': 0, 'region': ''}, + Point(position=0, offset=0, region=''), ) # Internal locus. @@ -121,37 +122,37 @@ def test_MultiLocus_inverted(): multi_locus.to_position, 35, multi_locus.to_coordinate, - {'position': 8, 'offset': -1, 'region': ''}, + Point(position=8, offset=-1, region=''), ) invariant( multi_locus.to_position, 34, multi_locus.to_coordinate, - {'position': 8, 'offset': 0, 'region': ''}, + Point(position=8, offset=0, region=''), ) invariant( multi_locus.to_position, 33, multi_locus.to_coordinate, - {'position': 9, 'offset': 0, 'region': ''}, + Point(position=9, offset=0, region=''), ) invariant( multi_locus.to_position, 31, multi_locus.to_coordinate, - {'position': 11, 'offset': 0, 'region': ''}, + Point(position=11, offset=0, region=''), ) invariant( multi_locus.to_position, 30, multi_locus.to_coordinate, - {'position': 12, 'offset': 0, 'region': ''}, + Point(position=12, offset=0, region=''), ) invariant( multi_locus.to_position, 29, multi_locus.to_coordinate, - {'position': 12, 'offset': 1, 'region': ''}, + Point(position=12, offset=1, region=''), ) # Boundary between the last locus and downstream. @@ -159,13 +160,13 @@ def test_MultiLocus_inverted(): multi_locus.to_position, 5, multi_locus.to_coordinate, - {'position': 21, 'offset': 0, 'region': ''}, + Point(position=21, offset=0, region=''), ) invariant( multi_locus.to_position, 4, multi_locus.to_coordinate, - {'position': 21, 'offset': 1, 'region': 'd'}, + Point(position=21, offset=1, region='d'), ) @@ -177,13 +178,13 @@ def test_MultiLocus_adjacent_loci(): multi_locus.to_position, 2, multi_locus.to_coordinate, - {'position': 1, 'offset': 0, 'region': ''}, + Point(position=1, offset=0, region=''), ) invariant( multi_locus.to_position, 3, multi_locus.to_coordinate, - {'position': 2, 'offset': 0, 'region': ''}, + Point(position=2, offset=0, region=''), ) @@ -195,13 +196,13 @@ def test_MultiLocus_adjacent_loci_inverted(): multi_locus.to_position, 3, multi_locus.to_coordinate, - {'position': 1, 'offset': 0, 'region': ''}, + Point(position=1, offset=0, region=''), ) invariant( multi_locus.to_position, 2, multi_locus.to_coordinate, - {'position': 2, 'offset': 0, 'region': ''}, + Point(position=2, offset=0, region=''), ) @@ -213,13 +214,13 @@ def test_MultiLocus_offsets_odd(): multi_locus.to_position, 4, multi_locus.to_coordinate, - {'position': 1, 'offset': 2, 'region': ''}, + Point(position=1, offset=2, region=''), ) invariant( multi_locus.to_position, 5, multi_locus.to_coordinate, - {'position': 2, 'offset': -1, 'region': ''}, + Point(position=2, offset=-1, region=''), ) @@ -231,13 +232,13 @@ def test_MultiLocus_offsets_odd_inverted(): multi_locus.to_position, 4, multi_locus.to_coordinate, - {'position': 1, 'offset': 2, 'region': ''}, + Point(position=1, offset=2, region=''), ) invariant( multi_locus.to_position, 3, multi_locus.to_coordinate, - {'position': 2, 'offset': -1, 'region': ''}, + Point(position=2, offset=-1, region=''), ) @@ -249,13 +250,13 @@ def test_MultiLocus_offsets_even(): multi_locus.to_position, 4, multi_locus.to_coordinate, - {'position': 1, 'offset': 2, 'region': ''}, + Point(position=1, offset=2, region=''), ) invariant( multi_locus.to_position, 5, multi_locus.to_coordinate, - {'position': 2, 'offset': -2, 'region': ''}, + Point(position=2, offset=-2, region=''), ) @@ -267,13 +268,13 @@ def test_MultiLocus_offsets_even_inverted(): multi_locus.to_position, 5, multi_locus.to_coordinate, - {'position': 1, 'offset': 2, 'region': ''}, + Point(position=1, offset=2, region=''), ) invariant( multi_locus.to_position, 4, multi_locus.to_coordinate, - {'position': 2, 'offset': -2, 'region': ''}, + Point(position=2, offset=-2, region=''), ) @@ -285,8 +286,8 @@ def test_MultiLocus_degenerate(): multi_locus.to_coordinate, 4, [ - {'position': 0, 'offset': -1, 'region': 'u'}, - {'position': -1, 'offset': 0, 'region': ''}, + Point(position=0, offset=-1, region='u'), + Point(position=-1, offset=0, region=''), ], ) @@ -294,9 +295,9 @@ def test_MultiLocus_degenerate(): multi_locus.to_coordinate, 72, [ - {'position': 21, 'offset': 1, 'region': 'd'}, - {'position': 22, 'offset': 0, 'region': ''}, - {'position': 22, 'offset': 1, 'region': 'd'}, + Point(position=21, offset=1, region='d'), + Point(position=22, offset=0, region=''), + Point(position=22, offset=1, region='d'), ], ) @@ -309,9 +310,9 @@ def test_MultiLocus_inverted_degenerate(): multi_locus.to_coordinate, 72, [ - {'position': -1, 'offset': 0, 'region': ''}, - {'position': 0, 'offset': -1, 'region': ''}, - {'position': 0, 'offset': -1, 'region': 'u'}, + Point(position=-1, offset=0, region=''), + Point(position=0, offset=-1, region=''), + Point(position=0, offset=-1, region='u'), ], ) @@ -319,8 +320,8 @@ def test_MultiLocus_inverted_degenerate(): multi_locus.to_coordinate, 4, [ - {'position': 21, 'offset': 1, 'region': ''}, - {'position': 22, 'offset': 0, 'region': ''}, - {'position': 21, 'offset': 1, 'region': 'd'}, + Point(position=21, offset=1, region=''), + Point(position=22, offset=0, region=''), + Point(position=21, offset=1, region='d'), ], ) From 157b92fb31c95e54e09c834e0c0ebc493c8d49f0 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 19 Jun 2026 17:32:20 +0200 Subject: [PATCH 153/236] Formatting. --- mutalyzer_crossmapper/crossmapper.py | 30 ++++++++++++++-------------- mutalyzer_crossmapper/locus.py | 1 + mutalyzer_crossmapper/models.py | 4 +--- mutalyzer_crossmapper/multi_locus.py | 2 -- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 632dde0..42efc5b 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -1,5 +1,5 @@ from .multi_locus import MultiLocus -from .models import GenomicPoint, NonCodingPoint, CodingPoint,ProteinPoint +from .models import GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint class Genomic(object): @@ -47,7 +47,7 @@ def coordinate_to_noncoding(self, coordinate: int) -> NonCodingPoint: return NonCodingPoint( position=point.position + 1, offset=point.offset, - region=point.region, + region=point.region ) def noncoding_to_coordinate(self, point: NonCodingPoint) -> int: @@ -61,7 +61,7 @@ def noncoding_to_coordinate(self, point: NonCodingPoint) -> int: NonCodingPoint( position=point.position - 1, offset=point.offset, - region=point.region, + region=point.region ) ) @@ -89,20 +89,20 @@ def __init__( if self._inverted: self._coding = ( cds_end.position + cds_end.offset, - cds_start.position + cds_start.offset + 1, + cds_start.position + cds_start.offset + 1 ) self._exons = ( exon_end.position + exon_end.offset, - exon_start.position + exon_start.offset + 1, + exon_start.position + exon_start.offset + 1 ) else: self._coding = ( cds_start.position + cds_start.offset, - cds_end.position + cds_end.offset + 1, + cds_end.position + cds_end.offset + 1 ) self._exons = ( exon_start.position + exon_start.offset, - exon_end.position + exon_end.offset + 1, + exon_end.position + exon_end.offset + 1 ) def _coordinate_to_coding(self, coordinate: int) -> CodingPoint: @@ -185,7 +185,7 @@ def coding_to_coordinate(self, point: CodingPoint) -> int: noncoding_point = NonCodingPoint( position=position + self._coding[0] - 1, offset=point.offset, - region='', + region='' ) return self._noncoding.to_coordinate(noncoding_point) @@ -195,14 +195,14 @@ def coding_to_coordinate(self, point: CodingPoint) -> int: NonCodingPoint( position=self._coding[0] - position, offset=point.offset, - region='', + region='' ) ) return self._noncoding.to_coordinate( NonCodingPoint( position=0, offset=point.offset + 1 - position, - region='u', + region='u' ) ) @@ -210,7 +210,7 @@ def coding_to_coordinate(self, point: CodingPoint) -> int: NonCodingPoint( position=self._coding[1] + position - 1, offset=point.offset, - region='', + region='' ) ) @@ -229,13 +229,13 @@ def coordinate_to_protein(self, coordinate: int) -> ProteinPoint: position=abs(-position // 3), position_in_codon=-position % 3 + 1, region=point.region, - offset=point.offset, + offset=point.offset ) return ProteinPoint( position=(position + 2) // 3, position_in_codon=(position + 2) % 3 + 1, region=point.region, - offset=point.offset, + offset=point.offset ) def protein_to_coordinate(self, point: ProteinPoint) -> int: @@ -250,13 +250,13 @@ def protein_to_coordinate(self, point: ProteinPoint) -> int: CodingPoint( position=3 * point.position - point.position_in_codon + 1, offset=point.offset, - region=point.region, + region=point.region ) ) return self.coding_to_coordinate( CodingPoint( position=3 * point.position + point.position_in_codon - 3, offset=point.offset, - region=point.region, + region=point.region ) ) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index ea8a291..207a6b6 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -1,5 +1,6 @@ from .models import Point + class Locus(object): """Locus object.""" def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: diff --git a/mutalyzer_crossmapper/models.py b/mutalyzer_crossmapper/models.py index 8704dcf..e47f06d 100644 --- a/mutalyzer_crossmapper/models.py +++ b/mutalyzer_crossmapper/models.py @@ -1,7 +1,5 @@ from __future__ import annotations - -from dataclasses import asdict, dataclass -from typing import Any +from dataclasses import dataclass # Basic dataclass module for locus and multi_locus diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 13c1bd3..133c9f2 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -74,8 +74,6 @@ def to_coordinate(self, point: Point) -> int: :returns int: Coordinate. """ - - region = point.region if region == 'u': From dc418ee74b934d2f9b2da74c5ab83c696e6ac5e0 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 23 Jun 2026 14:10:12 +0200 Subject: [PATCH 154/236] Update docstring. --- mutalyzer_crossmapper/locus.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 207a6b6..b1db533 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -18,7 +18,7 @@ def to_position(self, coordinate: int) -> Point: :arg int coordinate: Coordinate. - :returns Point: Position point with 'position' and 'offset'. + :returns Point: Position point model. """ if self._inverted: if coordinate > self.boundary[1]: @@ -36,7 +36,7 @@ def to_position(self, coordinate: int) -> Point: def to_coordinate(self, point: Point) -> int: """Convert a point model to a coordinate. - :arg Point point: Point model with 'position' and 'offset'. + :arg Point point: Point model. :returns int: Coordinate. """ From 132533bafbad35d4f107a9caceb6879496922af7 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 23 Jun 2026 14:23:41 +0200 Subject: [PATCH 155/236] multi_locus: update docstring for dataclass usage. --- mutalyzer_crossmapper/multi_locus.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 133c9f2..3de63ae 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -10,7 +10,7 @@ def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: """For each location, calculate the length of the preceding locations. :arg list locations: List of locations. - :arg int orientation: Direction of {locations}. + :arg int orientation: Direction of locations. :returns list: List of cumulative location lengths. """ @@ -55,7 +55,7 @@ def to_position(self, coordinate: int) -> Point: :arg int coordinate: Coordinate. - :returns Point: CodingPoint model with 'position', 'offset', and 'region'. + :returns Point: Point model . """ index = nearest_location(self._locations, coordinate, self._inverted) outside = self._orientation * self.outside(coordinate) @@ -70,7 +70,7 @@ def to_position(self, coordinate: int) -> Point: def to_coordinate(self, point: Point) -> int: """Convert a point model to a coordinate. - :arg CodingPoint point: Point model with 'position', 'offset', and 'region'. + :arg Point point: Point model. :returns int: Coordinate. """ From c8b4e661529007610b5ab3c6a86a193084fec199 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 23 Jun 2026 15:25:25 +0200 Subject: [PATCH 156/236] test_models: Add tests for dataclass models. --- tests/test_models.py | 154 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 tests/test_models.py diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..a50fb44 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,154 @@ +import pytest + +from mutalyzer_crossmapper.models import Point, CodingPoint, GenomicPoint, NonCodingPoint, Point, ProteinPoint + +# Point tests +def test_point_valid_creation(): + p = Point(position=-10) + assert p.position == -10 + assert p.offset == 0 + assert p.region == '' + + +def test_point_custom_values(): + p = Point(position=0, offset=4, region='*') + assert p.position == 0 + assert p.offset == 4 + assert p.region == '*' + + +def test_genomic_point_valid_creation(): + p = GenomicPoint(position=10) + assert p.position == 10 + + +def test_genomic_point_invalid_creation(): + with pytest.raises(ValueError): + GenomicPoint(position=-1) + + with pytest.raises(ValueError): + GenomicPoint(position=1.0) + + with pytest.raises(ValueError): + GenomicPoint(position='chr1') + + with pytest.raises(TypeError): + GenomicPoint(**{}) + + +def test_genomic_point_invalid_keyword(): + with pytest.raises(TypeError): + GenomicPoint(position=1, region='') + + with pytest.raises(TypeError): + GenomicPoint(**{'123': "abc"}) + + +def test_genomic_point_to_string(): + assert str(GenomicPoint(123456789)) == "123456789" + + +# NonCodingPoint tests +def test_noncoding_point_default_values(): + p = NonCodingPoint(position=10) + assert p.position == 10 + assert p.offset == 0 + assert p.region == '' + + +def test_noncoding_point_valid_creation(): + p = NonCodingPoint(position=10, offset=1, region='u') + assert p.position == 10 + assert p.offset == 1 + assert p.region == 'u' + + +def test_noncoding_point_invalid_creation(): + with pytest.raises(ValueError): + NonCodingPoint(position=0) + + with pytest.raises(ValueError): + NonCodingPoint(position=10, region='-') + + with pytest.raises(ValueError): + NonCodingPoint(position=10, region=123) + + with pytest.raises(TypeError): + NonCodingPoint(position=1, offset='+1') + + with pytest.raises(TypeError): + NonCodingPoint(offset=0) + + +def test_noncoding_point_invalid_keyword(): + with pytest.raises(TypeError): + NonCodingPoint(position=10, other='test') + + +def test_noncoding_point_to_string(): + assert str(NonCodingPoint(position=123, offset=0)) == '123' + assert str(NonCodingPoint(position=123, offset=11)) == '123+11' + assert str(NonCodingPoint(position=123, region='u')) == 'u123' + assert str(NonCodingPoint(position=123, offset=-10, region='')) == '123-10' + assert str(NonCodingPoint(position=123, offset=-11, region='d')) == 'd123-11' + + +# CodingPoint tests +def test_coding_point_default_creation(): + p = CodingPoint(position=11) + assert p.position == 11 + assert p.offset == 0 + assert p.region == '' + + +def test_coding_point_valid_creation(): + p = CodingPoint(position=987654321, offset=-1111, region='-') + assert p.position == 987654321 + assert p.offset == -1111 + assert p.region == '-' + +def test_coding_point_invalid_creation(): + with pytest.raises(ValueError): + CodingPoint(position=0, offset=-1, region='') + + +def test_coding_point_to_string(): + assert str(CodingPoint(position=123, offset=0)) == '123' + assert str(CodingPoint(position=123, offset=11)) == '123+11' + assert str(CodingPoint(position=123, offset=11, region='-')) == '-123+11' + assert str(CodingPoint(position=123, offset=11, region='*')) == '*123+11' + assert str(CodingPoint(position=123, offset=-11, region='-')) == '-123-11' + assert str(CodingPoint(position=123, region='u')) == 'u123' + assert str(CodingPoint(position=123, offset=-10, region='')) == '123-10' + assert str(CodingPoint(position=123, offset=-11, region='d')) == 'd123-11' + + +# ProteinPoint tests +def test_protein_point_default_position_in_codon(): + p = ProteinPoint(position=10) + assert p.position == 10 + assert p.region == '' + assert p.offset == 0 + assert p.position_in_codon == 1 + + +def test_protein_point_valid_creation(): + p = ProteinPoint(position=10, position_in_codon=2) + assert p.position == 10 + assert p.region == '' + assert p.offset == 0 + assert p.position_in_codon == 2 + + +def test_protein_point_invalid_creation(): + with pytest.raises(ValueError): + ProteinPoint(position=10, position_in_codon=0) + + +def test_protein_point_to_string(): + assert str(ProteinPoint(position=11)) == '11' + assert str(ProteinPoint(position=11, offset=0, region='', position_in_codon=2)) == '11' + assert str(ProteinPoint(position=11, offset=1, region='*')) == '*11+1' + assert str(ProteinPoint(position=11, offset=-1, region='-', position_in_codon=3)) == '-11-1' + assert str(ProteinPoint(position=11, offset=0, region='d')) == 'd11' + assert str(ProteinPoint(position=11, region='u', offset=10)) == 'u11+10' From a6d83939619979d6b1357a62d66bae2f682e8856 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 23 Jun 2026 15:38:50 +0200 Subject: [PATCH 157/236] Update models and use Point dataclass when use locus/multilocus modules. --- mutalyzer_crossmapper/__init__.py | 2 +- mutalyzer_crossmapper/crossmapper.py | 54 ++++++++-------------------- mutalyzer_crossmapper/models.py | 44 ++++++++++------------- 3 files changed, 34 insertions(+), 66 deletions(-) diff --git a/mutalyzer_crossmapper/__init__.py b/mutalyzer_crossmapper/__init__.py index fc079a8..fcf70d7 100644 --- a/mutalyzer_crossmapper/__init__.py +++ b/mutalyzer_crossmapper/__init__.py @@ -4,7 +4,7 @@ from .location import nearest_location from .locus import Locus from .multi_locus import MultiLocus -from .models import GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint +from .models import Point, GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint def _get_metadata(name: str) -> str: diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 42efc5b..3dea5d2 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -1,5 +1,5 @@ from .multi_locus import MultiLocus -from .models import GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint +from .models import Point, GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint class Genomic(object): @@ -58,7 +58,7 @@ def noncoding_to_coordinate(self, point: NonCodingPoint) -> int: :returns int: Coordinate. """ return self._noncoding.to_coordinate( - NonCodingPoint( + Point( position=point.position - 1, offset=point.offset, region=point.region @@ -106,7 +106,7 @@ def __init__( ) def _coordinate_to_coding(self, coordinate: int) -> CodingPoint: - """Convert a coordinate to a coding position (c./r.). + """Convert a coordinate to a coding point model (c./r.). :arg int coordinate: Coordinate. @@ -140,12 +140,12 @@ def _coordinate_to_coding(self, coordinate: int) -> CodingPoint: return CodingPoint(position=position, offset=offset, region=region) def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> CodingPoint: - """Convert a coordinate to a coding position (c./r.). + """Convert a coordinate to a coding point model (c./r.). :arg int coordinate: Coordinate. :arg bool degenerate: Return a degenerate position. - :returns CodingPoint: Coding position model (c./r.). + :returns CodingPoint: Coding point model (c./r.). """ point = self._coordinate_to_coding(coordinate) @@ -170,11 +170,10 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> Cod def coding_to_coordinate(self, point: CodingPoint) -> int: """Convert a coding position (c./r.) to a coordinate. - :arg CodingPoint point: Coding position model (c./r.). + :arg CodingPoint point: Coding point model (c./r.). :returns int: Coordinate. """ - region = point.region if region in ('u', 'd'): @@ -182,44 +181,21 @@ def coding_to_coordinate(self, point: CodingPoint) -> int: position = point.position if region == '': - noncoding_point = NonCodingPoint( - position=position + self._coding[0] - 1, - offset=point.offset, - region='' - ) - return self._noncoding.to_coordinate(noncoding_point) - - if region == '-': - if position <= self._coding[0]: - return self._noncoding.to_coordinate( - NonCodingPoint( - position=self._coding[0] - position, - offset=point.offset, - region='' - ) - ) - return self._noncoding.to_coordinate( - NonCodingPoint( - position=0, - offset=point.offset + 1 - position, - region='u' - ) - ) - + position = position + self._coding[0] - 1 + elif region == '-': + position = self._coding[0] - position + elif region == '*': + position = self._coding[1] + position - 1 return self._noncoding.to_coordinate( - NonCodingPoint( - position=self._coding[1] + position - 1, - offset=point.offset, - region='' - ) + Point(position=position, region='', offset=point.offset) ) def coordinate_to_protein(self, coordinate: int) -> ProteinPoint: - """Convert a coordinate to a protein position (p.). + """Convert a coordinate to a protein point model (p.). :arg int coordinate: Coordinate. - :returns ProteinPoint: Protein position model(p.). + :returns ProteinPoint: Protein point model(p.). """ point = self.coordinate_to_coding(coordinate) @@ -241,7 +217,7 @@ def coordinate_to_protein(self, coordinate: int) -> ProteinPoint: def protein_to_coordinate(self, point: ProteinPoint) -> int: """Convert a protein position (p.) to a coordinate. - :arg ProteinPoint point: Protein position model(p.). + :arg ProteinPoint point: Protein point model(p.). :returns int: Coordinate. """ diff --git a/mutalyzer_crossmapper/models.py b/mutalyzer_crossmapper/models.py index e47f06d..f6a21ea 100644 --- a/mutalyzer_crossmapper/models.py +++ b/mutalyzer_crossmapper/models.py @@ -2,31 +2,31 @@ from dataclasses import dataclass +def slotted_dataclass(cls=None, **kwargs): + return dataclass(cls, slots=True, **kwargs) + + # Basic dataclass module for locus and multi_locus -@dataclass(slots=True) +@slotted_dataclass class Point: position: int offset: int = 0 region: str = '' -@dataclass(slots=True) +@slotted_dataclass class GenomicPoint: position: int def __post_init__(self) -> None: - self._validate_position(self.position) + if not isinstance(self.position, int) or self.position <= 0: + raise ValueError('Position must be a positive integer') def __str__(self) -> str: return f"{self.position}" - @staticmethod - def _validate_position(position: int) -> None: - if not isinstance(position, int) or position < 0: - raise ValueError('position must be a non-negative integer') - -@dataclass(slots=True) +@slotted_dataclass class NonCodingPoint(GenomicPoint): offset: int = 0 region: str = '' @@ -34,18 +34,13 @@ class NonCodingPoint(GenomicPoint): allowed_regions = {'', 'u', 'd'} def __post_init__(self) -> None: + # Python version 3.11 and 3.10: cannot use super() due to conflicts with slots=True GenomicPoint.__post_init__(self) - self._validate_offset(self.offset) - self._validate_region(self.region) - - @staticmethod - def _validate_offset(offset: int) -> None: - if not isinstance(offset, int): - raise TypeError('offset must be an integer') - def _validate_region(self, region: str) -> None: - if not isinstance(region, str) or region not in self.allowed_regions: - raise ValueError(f'region must be a string in {self.allowed_regions}') + if not isinstance(self.offset, int): + raise TypeError('Offset must be an integer') + if not isinstance(self.region, str) or self.region not in self.allowed_regions: + raise ValueError(f'Region must be a string in {self.allowed_regions}') def __str__(self) -> str: if self.offset == 0: @@ -53,20 +48,17 @@ def __str__(self) -> str: return f"{self.region}{self.position}{self.offset:+}" -@dataclass(slots=True) +@slotted_dataclass class CodingPoint(NonCodingPoint): allowed_regions = {'', 'u', 'd', '-', '*'} -@dataclass(slots=True) +@slotted_dataclass class ProteinPoint(CodingPoint): position_in_codon: int = 1 def __post_init__(self) -> None: CodingPoint.__post_init__(self) - self._validate_position_in_codon(self.position_in_codon) - @staticmethod - def _validate_position_in_codon(position_in_codon: int) -> None: - if not isinstance(position_in_codon, int) or position_in_codon not in (1, 2, 3): - raise ValueError('position_in_codon must be 1, 2, or 3') + if not isinstance(self.position_in_codon, int) or self.position_in_codon not in (1, 2, 3): + raise ValueError('Position_in_codon must be 1, 2, or 3') From b4aae459b5e5c149625fc134ec50122e8ab98816 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 24 Jun 2026 11:35:43 +0200 Subject: [PATCH 158/236] Move dataclass from models to locus and crossmapper. --- mutalyzer_crossmapper/__init__.py | 3 +- mutalyzer_crossmapper/crossmapper.py | 58 +++++++++- mutalyzer_crossmapper/locus.py | 8 ++ mutalyzer_crossmapper/models.py | 64 ----------- tests/test_crossmapper.py | 3 +- tests/test_models.py | 154 --------------------------- 6 files changed, 67 insertions(+), 223 deletions(-) delete mode 100644 mutalyzer_crossmapper/models.py delete mode 100644 tests/test_models.py diff --git a/mutalyzer_crossmapper/__init__.py b/mutalyzer_crossmapper/__init__.py index fcf70d7..66f391d 100644 --- a/mutalyzer_crossmapper/__init__.py +++ b/mutalyzer_crossmapper/__init__.py @@ -1,10 +1,9 @@ from importlib.metadata import metadata -from .crossmapper import Coding, Genomic, NonCoding +from .crossmapper import Coding, Genomic, NonCoding, GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint from .location import nearest_location from .locus import Locus from .multi_locus import MultiLocus -from .models import Point, GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint def _get_metadata(name: str) -> str: diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 3dea5d2..b1b545d 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -1,5 +1,20 @@ +from dataclasses import dataclass + from .multi_locus import MultiLocus -from .models import Point, GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint +from .models import Point + + +@dataclass(slots=True) +class GenomicPoint: + """Genomic dataclass.""" + position: int + + def __post_init__(self) -> None: + if not isinstance(self.position, int) or self.position <= 0: + raise ValueError('Position must be a positive integer') + + def __str__(self) -> str: + return f'{self.position}' class Genomic(object): @@ -24,6 +39,29 @@ def genomic_to_coordinate(self, point: GenomicPoint) -> int: return point.position - 1 +@dataclass(slots=True) +class NonCodingPoint(GenomicPoint): + """NonCoding dataclass.""" + offset: int = 0 + region: str = '' + + allowed_regions = {'', 'u', 'd'} + + def __post_init__(self) -> None: + # Python version 3.11 and 3.10: cannot use super() due to conflicts with slots=True + GenomicPoint.__post_init__(self) + + if not isinstance(self.offset, int): + raise TypeError('Offset must be an integer') + if not isinstance(self.region, str) or self.region not in self.allowed_regions: + raise ValueError(f'Region must be a string in {self.allowed_regions}') + + def __str__(self) -> str: + if self.offset == 0: + return f'{self.region}{self.position}' + return f'{self.region}{self.position}{self.offset:+}' + + class NonCoding(Genomic): """NonCoding crossmap object.""" @@ -66,6 +104,24 @@ def noncoding_to_coordinate(self, point: NonCodingPoint) -> int: ) +@dataclass(slots=True) +class CodingPoint(NonCodingPoint): + """Coding dataclass.""" + allowed_regions = {'', 'u', 'd', '-', '*'} + + +@dataclass(slots=True) +class ProteinPoint(CodingPoint): + """Protein dataclass.""" + position_in_codon: int = 1 + + def __post_init__(self) -> None: + CodingPoint.__post_init__(self) + + if not isinstance(self.position_in_codon, int) or self.position_in_codon not in (1, 2, 3): + raise ValueError('Position_in_codon must be 1, 2, or 3') + + class Coding(NonCoding): """Coding crossmap object.""" def __init__( diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index b1db533..3c0011e 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -1,8 +1,16 @@ from .models import Point +from dataclasses import dataclass class Locus(object): """Locus object.""" + + @dataclass(slots=True) + class Point: + position: int + offset: int = 0 + region: str = '' + def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: """ :arg tuple location: Locus location. diff --git a/mutalyzer_crossmapper/models.py b/mutalyzer_crossmapper/models.py deleted file mode 100644 index f6a21ea..0000000 --- a/mutalyzer_crossmapper/models.py +++ /dev/null @@ -1,64 +0,0 @@ -from __future__ import annotations -from dataclasses import dataclass - - -def slotted_dataclass(cls=None, **kwargs): - return dataclass(cls, slots=True, **kwargs) - - -# Basic dataclass module for locus and multi_locus -@slotted_dataclass -class Point: - position: int - offset: int = 0 - region: str = '' - - -@slotted_dataclass -class GenomicPoint: - position: int - - def __post_init__(self) -> None: - if not isinstance(self.position, int) or self.position <= 0: - raise ValueError('Position must be a positive integer') - - def __str__(self) -> str: - return f"{self.position}" - - -@slotted_dataclass -class NonCodingPoint(GenomicPoint): - offset: int = 0 - region: str = '' - - allowed_regions = {'', 'u', 'd'} - - def __post_init__(self) -> None: - # Python version 3.11 and 3.10: cannot use super() due to conflicts with slots=True - GenomicPoint.__post_init__(self) - - if not isinstance(self.offset, int): - raise TypeError('Offset must be an integer') - if not isinstance(self.region, str) or self.region not in self.allowed_regions: - raise ValueError(f'Region must be a string in {self.allowed_regions}') - - def __str__(self) -> str: - if self.offset == 0: - return f"{self.region}{self.position}" - return f"{self.region}{self.position}{self.offset:+}" - - -@slotted_dataclass -class CodingPoint(NonCodingPoint): - allowed_regions = {'', 'u', 'd', '-', '*'} - - -@slotted_dataclass -class ProteinPoint(CodingPoint): - position_in_codon: int = 1 - - def __post_init__(self) -> None: - CodingPoint.__post_init__(self) - - if not isinstance(self.position_in_codon, int) or self.position_in_codon not in (1, 2, 3): - raise ValueError('Position_in_codon must be 1, 2, or 3') diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 2b6a92d..2ecd887 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -1,5 +1,4 @@ -from mutalyzer_crossmapper import Coding, Genomic, NonCoding -from mutalyzer_crossmapper.models import CodingPoint, GenomicPoint, NonCodingPoint, ProteinPoint +from mutalyzer_crossmapper import Genomic, NonCoding, Coding, GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint from helper import degenerate_equal, invariant diff --git a/tests/test_models.py b/tests/test_models.py deleted file mode 100644 index a50fb44..0000000 --- a/tests/test_models.py +++ /dev/null @@ -1,154 +0,0 @@ -import pytest - -from mutalyzer_crossmapper.models import Point, CodingPoint, GenomicPoint, NonCodingPoint, Point, ProteinPoint - -# Point tests -def test_point_valid_creation(): - p = Point(position=-10) - assert p.position == -10 - assert p.offset == 0 - assert p.region == '' - - -def test_point_custom_values(): - p = Point(position=0, offset=4, region='*') - assert p.position == 0 - assert p.offset == 4 - assert p.region == '*' - - -def test_genomic_point_valid_creation(): - p = GenomicPoint(position=10) - assert p.position == 10 - - -def test_genomic_point_invalid_creation(): - with pytest.raises(ValueError): - GenomicPoint(position=-1) - - with pytest.raises(ValueError): - GenomicPoint(position=1.0) - - with pytest.raises(ValueError): - GenomicPoint(position='chr1') - - with pytest.raises(TypeError): - GenomicPoint(**{}) - - -def test_genomic_point_invalid_keyword(): - with pytest.raises(TypeError): - GenomicPoint(position=1, region='') - - with pytest.raises(TypeError): - GenomicPoint(**{'123': "abc"}) - - -def test_genomic_point_to_string(): - assert str(GenomicPoint(123456789)) == "123456789" - - -# NonCodingPoint tests -def test_noncoding_point_default_values(): - p = NonCodingPoint(position=10) - assert p.position == 10 - assert p.offset == 0 - assert p.region == '' - - -def test_noncoding_point_valid_creation(): - p = NonCodingPoint(position=10, offset=1, region='u') - assert p.position == 10 - assert p.offset == 1 - assert p.region == 'u' - - -def test_noncoding_point_invalid_creation(): - with pytest.raises(ValueError): - NonCodingPoint(position=0) - - with pytest.raises(ValueError): - NonCodingPoint(position=10, region='-') - - with pytest.raises(ValueError): - NonCodingPoint(position=10, region=123) - - with pytest.raises(TypeError): - NonCodingPoint(position=1, offset='+1') - - with pytest.raises(TypeError): - NonCodingPoint(offset=0) - - -def test_noncoding_point_invalid_keyword(): - with pytest.raises(TypeError): - NonCodingPoint(position=10, other='test') - - -def test_noncoding_point_to_string(): - assert str(NonCodingPoint(position=123, offset=0)) == '123' - assert str(NonCodingPoint(position=123, offset=11)) == '123+11' - assert str(NonCodingPoint(position=123, region='u')) == 'u123' - assert str(NonCodingPoint(position=123, offset=-10, region='')) == '123-10' - assert str(NonCodingPoint(position=123, offset=-11, region='d')) == 'd123-11' - - -# CodingPoint tests -def test_coding_point_default_creation(): - p = CodingPoint(position=11) - assert p.position == 11 - assert p.offset == 0 - assert p.region == '' - - -def test_coding_point_valid_creation(): - p = CodingPoint(position=987654321, offset=-1111, region='-') - assert p.position == 987654321 - assert p.offset == -1111 - assert p.region == '-' - -def test_coding_point_invalid_creation(): - with pytest.raises(ValueError): - CodingPoint(position=0, offset=-1, region='') - - -def test_coding_point_to_string(): - assert str(CodingPoint(position=123, offset=0)) == '123' - assert str(CodingPoint(position=123, offset=11)) == '123+11' - assert str(CodingPoint(position=123, offset=11, region='-')) == '-123+11' - assert str(CodingPoint(position=123, offset=11, region='*')) == '*123+11' - assert str(CodingPoint(position=123, offset=-11, region='-')) == '-123-11' - assert str(CodingPoint(position=123, region='u')) == 'u123' - assert str(CodingPoint(position=123, offset=-10, region='')) == '123-10' - assert str(CodingPoint(position=123, offset=-11, region='d')) == 'd123-11' - - -# ProteinPoint tests -def test_protein_point_default_position_in_codon(): - p = ProteinPoint(position=10) - assert p.position == 10 - assert p.region == '' - assert p.offset == 0 - assert p.position_in_codon == 1 - - -def test_protein_point_valid_creation(): - p = ProteinPoint(position=10, position_in_codon=2) - assert p.position == 10 - assert p.region == '' - assert p.offset == 0 - assert p.position_in_codon == 2 - - -def test_protein_point_invalid_creation(): - with pytest.raises(ValueError): - ProteinPoint(position=10, position_in_codon=0) - - -def test_protein_point_to_string(): - assert str(ProteinPoint(position=11)) == '11' - assert str(ProteinPoint(position=11, offset=0, region='', position_in_codon=2)) == '11' - assert str(ProteinPoint(position=11, offset=1, region='*')) == '*11+1' - assert str(ProteinPoint(position=11, offset=-1, region='-', position_in_codon=3)) == '-11-1' - assert str(ProteinPoint(position=11, offset=0, region='d')) == 'd11' - assert str(ProteinPoint(position=11, region='u', offset=10)) == 'u11+10' From 1ac6274596477d7ec3f733bc14b784c357e427d3 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 24 Jun 2026 11:55:55 +0200 Subject: [PATCH 159/236] Correct import. --- mutalyzer_crossmapper/crossmapper.py | 3 +-- mutalyzer_crossmapper/locus.py | 15 ++++++++------- mutalyzer_crossmapper/multi_locus.py | 3 +-- tests/test_locus.py | 2 +- tests/test_multi_locus.py | 2 +- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index b1b545d..beab92e 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -1,8 +1,7 @@ from dataclasses import dataclass from .multi_locus import MultiLocus -from .models import Point - +from .locus import Point @dataclass(slots=True) class GenomicPoint: diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 3c0011e..d7582bf 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -1,16 +1,17 @@ -from .models import Point from dataclasses import dataclass +@dataclass(slots=True) +class Point: + """Point dataclass""" + position: int + offset: int = 0 + region: str = '' + + class Locus(object): """Locus object.""" - @dataclass(slots=True) - class Point: - position: int - offset: int = 0 - region: str = '' - def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: """ :arg tuple location: Locus location. diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 3de63ae..26d699a 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -2,8 +2,7 @@ from itertools import accumulate from .location import nearest_location -from .locus import Locus -from .models import Point +from .locus import Locus, Point def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: diff --git a/tests/test_locus.py b/tests/test_locus.py index 2379125..07830bb 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -1,5 +1,5 @@ from mutalyzer_crossmapper import Locus -from mutalyzer_crossmapper.models import Point +from mutalyzer_crossmapper.locus import Point from helper import degenerate_equal, invariant diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index ce02704..f076e81 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -1,6 +1,6 @@ from mutalyzer_crossmapper import MultiLocus from mutalyzer_crossmapper.multi_locus import _offsets -from mutalyzer_crossmapper.models import Point +from mutalyzer_crossmapper.locus import Point from helper import degenerate_equal, invariant From 7bbabe46f8b5a48eb21456e28cba078a96a7d45e Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 25 Jun 2026 15:21:59 +0200 Subject: [PATCH 160/236] Fix typing error and add test in CI. --- .github/workflows/python-package.yml | 6 +++++- mutalyzer_crossmapper/crossmapper.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 72a1df3..9a934f2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -1,7 +1,7 @@ name: build on: push: - branches: + branches: - master pull_request: branches: @@ -30,6 +30,10 @@ jobs: run: | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test typings + run: | + pip install mypy + mypy --strict mutalyzer_crossmapper - name: Test with pytest run: | pytest diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index beab92e..914e74b 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -232,7 +232,9 @@ def coding_to_coordinate(self, point: CodingPoint) -> int: region = point.region if region in ('u', 'd'): - return self._noncoding.to_coordinate(point) + return self._noncoding.to_coordinate( + Point(position=point.position, region=point.region, offset=point.offset) + ) position = point.position if region == '': From 08f22617cdc4628656e3a276767e40c4c525fa72 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 25 Jun 2026 15:44:45 +0200 Subject: [PATCH 161/236] Test importlib_metadata to avoid mypy error from python v3.10 and v3.11. --- mutalyzer_crossmapper/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mutalyzer_crossmapper/__init__.py b/mutalyzer_crossmapper/__init__.py index 66f391d..a029e87 100644 --- a/mutalyzer_crossmapper/__init__.py +++ b/mutalyzer_crossmapper/__init__.py @@ -1,4 +1,5 @@ -from importlib.metadata import metadata +# from importlib.metadata import metadata +from importlib_metadata import metadata from .crossmapper import Coding, Genomic, NonCoding, GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint from .location import nearest_location From a9fdf97aa6ce165b4268800e94337937f9834363 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 1 Jul 2026 16:46:31 +0200 Subject: [PATCH 162/236] Discard redundant checking and overwrite serialization for ProteinPoint. --- mutalyzer_crossmapper/crossmapper.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 914e74b..0aeeae4 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -52,7 +52,7 @@ def __post_init__(self) -> None: if not isinstance(self.offset, int): raise TypeError('Offset must be an integer') - if not isinstance(self.region, str) or self.region not in self.allowed_regions: + if self.region not in self.allowed_regions: raise ValueError(f'Region must be a string in {self.allowed_regions}') def __str__(self) -> str: @@ -120,6 +120,11 @@ def __post_init__(self) -> None: if not isinstance(self.position_in_codon, int) or self.position_in_codon not in (1, 2, 3): raise ValueError('Position_in_codon must be 1, 2, or 3') + def __str__(self) -> str: + if self.offset == 0 and self.region == '': + return f'{self.position}' + return '?' + class Coding(NonCoding): """Coding crossmap object.""" @@ -162,6 +167,7 @@ def __init__( def _coordinate_to_coding(self, coordinate: int) -> CodingPoint: """Convert a coordinate to a coding point model (c./r.). + #TODO: explain why checking :arg int coordinate: Coordinate. @@ -196,6 +202,7 @@ def _coordinate_to_coding(self, coordinate: int) -> CodingPoint: def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> CodingPoint: """Convert a coordinate to a coding point model (c./r.). + # TODO: explain abs() :arg int coordinate: Coordinate. :arg bool degenerate: Return a degenerate position. @@ -204,21 +211,21 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> Cod """ point = self._coordinate_to_coding(coordinate) - region = point.region if not degenerate: return point - if region == 'u': + offset = abs(point.offset) + if point.region == 'u': if self._coding[0] == 0: - position = abs(point.offset) + position = offset else: - position = point.position + abs(point.offset) + position = point.position + offset return CodingPoint(position=position, offset=0, region='-') - if region == 'd': + if point.region == 'd': if self._exons[1] == self._coding[1]: - position = abs(point.offset) + position = offset else: - position = point.position + abs(point.offset) + position = point.position + offset return CodingPoint(position=position, offset=0, region='*') return point From a9124559f15753573f4ac2b2b5ac2ea30cf473ab Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 7 Jul 2026 12:05:17 +0200 Subject: [PATCH 163/236] Use back metadata in importlib. --- mutalyzer_crossmapper/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mutalyzer_crossmapper/__init__.py b/mutalyzer_crossmapper/__init__.py index a029e87..66f391d 100644 --- a/mutalyzer_crossmapper/__init__.py +++ b/mutalyzer_crossmapper/__init__.py @@ -1,5 +1,4 @@ -# from importlib.metadata import metadata -from importlib_metadata import metadata +from importlib.metadata import metadata from .crossmapper import Coding, Genomic, NonCoding, GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint from .location import nearest_location From cc65a7bf23f44db338805b1e19b0cffeebc08eca Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 7 Jul 2026 12:30:59 +0200 Subject: [PATCH 164/236] Multi_locus: Add checks to validate input coordinate and input point. --- mutalyzer_crossmapper/multi_locus.py | 51 ++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 26d699a..5b64d92 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -1,5 +1,6 @@ from bisect import bisect_right from itertools import accumulate +from operator import index from .location import nearest_location from .locus import Locus, Point @@ -24,6 +25,9 @@ def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> :arg list locations: List of locus locations. :arg bool inverted: Orientation. """ + # Check if the locations are non-overlapping , e.g., [(1, 5), (4, 10)] should be invalid; + # and sorted e.g., [(1, 5), (10, 15), (5, 10)] should be invalid. + # Look for circular chromosome sequence self._locations = locations self._inverted = inverted @@ -31,6 +35,44 @@ def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> self._orientation = -1 if inverted else 1 self._offsets = _offsets(locations, self._orientation) + # Consider add the length of the sequnce to the MultiLocus object, + # so that we can check if a coordinate is outside the sequence length. + def _validate_coordinate(self, coordinate: int) -> None: + """Check if a coordinate is within the MultiLocus. + + :arg int coordinate: Coordinate. + + :raises ValueError: If coordinate is outside the MultiLocus. + """ + if coordinate < 0: + raise IndexError("Coordinate is outside sequence length.") + + def _validate_point(self, index: int, point: Point) -> None: + """Check if a point is valid. + + :arg int index: Index of the locus. + :arg Point point: Point model. + + :raises ValueError: If point is outside the MultiLocus. + """ + if index == 0 and abs(point.offset) > self._loci[0].boundary[0]: + raise IndexError(f"Offset {point.offset} is outside the intron length {self._loci[0].boundary[0]}.") + if index > 0 and abs(point.offset) > self._loci[index].boundary[0] - self._loci[index - 1].boundary[1]: + raise IndexError(f"Offset {point.offset} is outside the intron length {self._loci[index].boundary[0] - self._loci[index - 1].boundary[1]}.") + + if point.offset < 0: + if point.position not in self._loci[index].boundary: + raise ValueError(f"Position {point.position} is not at an exon boundary.") + if self._loci[self._direction(index)].boundary[0] != point.position: + raise IndexError(f"Offset {point.offset} should be '-' when position is at exon start.") + + if point.offset > 0: + if point.position not in self._loci[index].boundary: + raise ValueError(f"Position {point.position} is not at an exon boundary.") + if self._loci[self._direction(index)].boundary[1] != point.position: + raise IndexError(f"Offset {point.offset} should be '+' when position is at exon end.") + + def _direction(self, index: int) -> int: if self._inverted: return len(self._offsets) - index - 1 @@ -56,6 +98,7 @@ def to_position(self, coordinate: int) -> Point: :returns Point: Point model . """ + self._validate_coordinate(coordinate) index = nearest_location(self._locations, coordinate, self._inverted) outside = self._orientation * self.outside(coordinate) region = 'u' if outside < 0 else 'd' if outside > 0 else '' @@ -73,13 +116,14 @@ def to_coordinate(self, point: Point) -> int: :returns int: Coordinate. """ - region = point.region - if region == 'u': + + if point.region == 'u': if self._inverted: return self._locations[-1][1] - point.offset - 1 + self._validate_point(0, point) return self._locations[0][0] + point.offset - if region == 'd': + if point.region == 'd': if self._inverted: return self._locations[0][0] - point.offset return self._locations[-1][1] + point.offset - 1 @@ -88,6 +132,7 @@ def to_coordinate(self, point: Point) -> int: len(self._offsets), max(0, bisect_right(self._offsets, point.position) - 1) ) + self._validate_point(index, point) return self._loci[self._direction(index)].to_coordinate( Point( position=point.position - self._offsets[index], From d2200e325dafbabc7e3b6ae78ea6a488659978be Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 17 Jul 2026 12:12:42 +0200 Subject: [PATCH 165/236] Locus.py: Add checks. --- mutalyzer_crossmapper/locus.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index d7582bf..b9c8e2a 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from .checker import _check_locus, _check_coordinate @dataclass(slots=True) @@ -6,7 +7,9 @@ class Point: """Point dataclass""" position: int offset: int = 0 - region: str = '' + + def __post_init__(self) -> None: + _check_coordinate(self.position) class Locus(object): @@ -17,9 +20,10 @@ def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: :arg tuple location: Locus location. :arg bool inverted: Orientation. """ - self._inverted = inverted + _check_locus(location) - self.boundary = location[0], location[1] - 1 + self._inverted = inverted + self.boundary = location[0], location[1] - 1 #: 0 based open on coordinate self._end = self.boundary[1] - self.boundary[0] def to_position(self, coordinate: int) -> Point: @@ -29,6 +33,7 @@ def to_position(self, coordinate: int) -> Point: :returns Point: Position point model. """ + _check_coordinate(coordinate) if self._inverted: if coordinate > self.boundary[1]: return Point(position=0, offset=self.boundary[1] - coordinate) @@ -49,6 +54,15 @@ def to_coordinate(self, point: Point) -> int: :returns int: Coordinate. """ + if point.offset != 0 and point.position not in (0, self._end): + raise ValueError(f"Position {point.position} is not at locus boundary.") + if point.offset < 0 and point.position != 0: + raise IndexError(f"Offset {point.offset} at locus start should be negative.") + if point.offset > 0 and point.position != self._end: + raise IndexError(f"Offset {point.offset} at locus end should be positive.") + if point.position > self._end: + raise IndexError(f"Position {point.position} exceeds locus length {self._end + 1}") + if self._inverted: return self.boundary[1] - point.position - point.offset return self.boundary[0] + point.position + point.offset From d10f1b82de2a8bd8988e040fb0d7a0b7781b6187 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 14 Aug 2026 09:50:43 +0200 Subject: [PATCH 166/236] Add checks in locus module. --- mutalyzer_crossmapper/checker.py | 84 ++++++++++++++++++++++++ mutalyzer_crossmapper/locus.py | 73 +++++++++++++-------- tests/test_locus.py | 108 ++++++++++++++++++++++++------- 3 files changed, 212 insertions(+), 53 deletions(-) create mode 100644 mutalyzer_crossmapper/checker.py diff --git a/mutalyzer_crossmapper/checker.py b/mutalyzer_crossmapper/checker.py new file mode 100644 index 0000000..d82ef97 --- /dev/null +++ b/mutalyzer_crossmapper/checker.py @@ -0,0 +1,84 @@ +from mutalyzer_crossmapper.location import nearest_location + + +def _check_int(value: int) -> None: + """Check if the value is a non-negative integer. + + :arg int value: Value to check. + + :raises ValueError: If the value is invalid. + """ + if not isinstance(value, int): + raise ValueError("Value must be an integer.") + + +def _check_in_range(value: int, length: int) -> None: + if value > length: + raise ValueError(f"Value {value} must be within the bounds of the reference sequence {length}.") + + +def _check_non_negative(value: int, length: int|None = None) -> None: + """Check if the coordinate is a non-negative integer. + + :arg int value: Value to check. + + :raises ValueError: If the coordinate is invalid. + """ + _check_int(value) + if value < 0: + raise ValueError("Value must be non-negative.") + if length is not None: + _check_in_range(value, length) + + +def _check_locus(locus: tuple[int, int], length: int| None = None) -> None: + """Check if the range is valid. + + :arg tuple[int, int] locus: Locus to check. + + :raises ValueError: If the range is invalid. + """ + if len(locus) != 2: + raise ValueError("Locus must be a tuple of two values.") + + for value in locus: + _check_non_negative(value, length) + + if locus[0] > locus[1]: + raise ValueError("Start of locus must be smaller than or equal to end of locus.") + + +def _check_exons(exons: list[tuple[int, int]], length: int|None = None) -> None: + """Check if the exons are valid. + The exons are valid + if they are a list of valid loci, + non-overlapping, + and within the bounds of the reference sequence. + + :arg list[tuple[int, int]] exons: Exons to check. + + :raises ValueError: If the exons are invalid. + """ + for exon in exons: + _check_locus(exon, length) + + for e1, e2 in zip(exons, exons[1:]): + if e2[0] < e1[1]: + raise ValueError(f"Exon {e2} and exon {e1} are overlapping.") + + +def _check_cds(cds: tuple[int, int], exons: list[tuple[int, int]], length: int|None = None) -> None: + """Check if the CDS is valid. + + :arg tuple[int, int] cds: CDS to check. + :arg list[tuple[int, int]] exons: List of exons. + :arg int|None length: Length of the reference sequence. + + :raises ValueError: If the CDS is invalid. + """ + _check_locus(cds, length) + for coord in cds: + index = nearest_location(exons, coord) + if coord < exons[index][0] or coord >= exons[index][1]: + raise ValueError(f"Coordinate {coord} of CDS {cds} is not within any exon.") + diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index b9c8e2a..24af131 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from .checker import _check_locus, _check_coordinate +from .checker import _check_locus, _check_non_negative, _check_int @dataclass(slots=True) @@ -9,7 +9,17 @@ class Point: offset: int = 0 def __post_init__(self) -> None: - _check_coordinate(self.position) + _check_non_negative(self.position) + _check_int(self.offset) + + +@dataclass(slots=True) +class Coord: + """Coordinate dataclass""" + coordinate: int + + def __post_init__(self) -> None: + _check_non_negative(self.coordinate) class Locus(object): @@ -26,43 +36,50 @@ def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: self.boundary = location[0], location[1] - 1 #: 0 based open on coordinate self._end = self.boundary[1] - self.boundary[0] - def to_position(self, coordinate: int) -> Point: + def _validate_point(self, position, offset) -> None: + """Validate a point model under HGVS rules. + + :arg int position: Position. + :arg int offset: Offset. + """ + if offset != 0 and position not in (0, self._end): + raise ValueError(f"Position {position} is not at locus boundary.") + if offset < 0 and position != 0: + raise IndexError(f"Offset {offset} should be at a locus start.") + if offset > 0 and position != self._end: + raise IndexError(f"Offset {offset} should be at a locus end.") + if position > self._end: + raise IndexError(f"Position {position} exceeds locus length {self._end + 1}") + + def to_position(self, coord: Coord) -> Point: """Convert a coordinate to a proper point model. - :arg int coordinate: Coordinate. + :arg Coord coord: Coordinate module. :returns Point: Position point model. """ - _check_coordinate(coordinate) if self._inverted: - if coordinate > self.boundary[1]: - return Point(position=0, offset=self.boundary[1] - coordinate) - if coordinate < self.boundary[0]: - return Point(position=self._end, offset=self.boundary[0] - coordinate) - return Point(position=self.boundary[1] - coordinate, offset=0) + if coord.coordinate > self.boundary[1]: + return Point(position=0, offset=self.boundary[1] - coord.coordinate) + if coord.coordinate < self.boundary[0]: + return Point(position=self._end, offset=self.boundary[0] - coord.coordinate) + return Point(position=self.boundary[1] - coord.coordinate, offset=0) - if coordinate < self.boundary[0]: - return Point(position=0, offset=coordinate - self.boundary[0]) - if coordinate > self.boundary[1]: - return Point(position=self._end, offset=coordinate - self.boundary[1]) - return Point(position=coordinate - self.boundary[0], offset=0) + if coord.coordinate < self.boundary[0]: + return Point(position=0, offset=coord.coordinate - self.boundary[0]) + if coord.coordinate > self.boundary[1]: + return Point(position=self._end, offset=coord.coordinate - self.boundary[1]) + return Point(position=coord.coordinate - self.boundary[0], offset=0) - def to_coordinate(self, point: Point) -> int: - """Convert a point model to a coordinate. + def to_coordinate(self, point: Point) -> Coord: + """Convert a point model to a coordinate model. :arg Point point: Point model. - :returns int: Coordinate. + :returns Coord: Coordinate model. """ - if point.offset != 0 and point.position not in (0, self._end): - raise ValueError(f"Position {point.position} is not at locus boundary.") - if point.offset < 0 and point.position != 0: - raise IndexError(f"Offset {point.offset} at locus start should be negative.") - if point.offset > 0 and point.position != self._end: - raise IndexError(f"Offset {point.offset} at locus end should be positive.") - if point.position > self._end: - raise IndexError(f"Position {point.position} exceeds locus length {self._end + 1}") + self._validate_point(point.position, point.offset) if self._inverted: - return self.boundary[1] - point.position - point.offset - return self.boundary[0] + point.position + point.offset + return Coord(coordinate=self.boundary[1] - point.position - point.offset) + return Coord(coordinate=self.boundary[0] + point.position + point.offset) diff --git a/tests/test_locus.py b/tests/test_locus.py index 07830bb..89419cc 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -1,44 +1,102 @@ from mutalyzer_crossmapper import Locus -from mutalyzer_crossmapper.locus import Point +from mutalyzer_crossmapper.locus import Point, Coord from helper import degenerate_equal, invariant +import pytest def test_Locus(): - """Forward orientent Lovus.""" + """Forward orientent Locus.""" locus = Locus((30, 35)) - invariant(locus.to_position, 29, locus.to_coordinate, Point(position=0, offset=-1)) - invariant(locus.to_position, 30, locus.to_coordinate, Point(position=0, offset=0)) - invariant(locus.to_position, 31, locus.to_coordinate, Point(position=1, offset=0)) - invariant(locus.to_position, 33, locus.to_coordinate, Point(position=3, offset=0)) - invariant(locus.to_position, 34, locus.to_coordinate, Point(position=4, offset=0)) - invariant(locus.to_position, 35, locus.to_coordinate, Point(position=4, offset=1)) + invariant(locus.to_position, Coord(29), locus.to_coordinate, Point(position=0, offset=-1)) + invariant(locus.to_position, Coord(30), locus.to_coordinate, Point(position=0, offset=0)) + invariant(locus.to_position, Coord(31), locus.to_coordinate, Point(position=1, offset=0)) + invariant(locus.to_position, Coord(33), locus.to_coordinate, Point(position=3, offset=0)) + invariant(locus.to_position, Coord(34), locus.to_coordinate, Point(position=4, offset=0)) + invariant(locus.to_position, Coord(35), locus.to_coordinate, Point(position=4, offset=1)) def test_Locus_inverted(): - """Reverse orientent Lovus.""" + """Reverse orientent Locus.""" locus = Locus((30, 35), True) - invariant(locus.to_position, 35, locus.to_coordinate, Point(position=0, offset=-1)) - invariant(locus.to_position, 34, locus.to_coordinate, Point(position=0, offset=0)) - invariant(locus.to_position, 33, locus.to_coordinate, Point(position=1, offset=0)) - invariant(locus.to_position, 31, locus.to_coordinate, Point(position=3, offset=0)) - invariant(locus.to_position, 30, locus.to_coordinate, Point(position=4, offset=0)) - invariant(locus.to_position, 29, locus.to_coordinate, Point(position=4, offset=1)) + invariant(locus.to_position, Coord(35), locus.to_coordinate, Point(position=0, offset=-1)) + invariant(locus.to_position, Coord(34), locus.to_coordinate, Point(position=0, offset=0)) + invariant(locus.to_position, Coord(33), locus.to_coordinate, Point(position=1, offset=0)) + invariant(locus.to_position, Coord(31), locus.to_coordinate, Point(position=3, offset=0)) + invariant(locus.to_position, Coord(30), locus.to_coordinate, Point(position=4, offset=0)) + invariant(locus.to_position, Coord(29), locus.to_coordinate, Point(position=4, offset=1)) +from mutalyzer_crossmapper.locus import Locus, Coord, Point as LocusPoint -def test_Locus_degenerate(): - """Degenerate positions are silently corrected.""" - locus = Locus((10, 20)) +## Test Locus model and its point model +def test_invalid_Locus_initialization(): + """Test Locus initialization.""" + with pytest.raises(ValueError): + Locus((10, 5)) + with pytest.raises(ValueError): + Locus((10, 20, 30)) + with pytest.raises(ValueError): + Locus((10, -5)) + with pytest.raises(ValueError): + Locus((10, 20.5)) + with pytest.raises(ValueError): + Locus((10.5, None)) + with pytest.raises(ValueError): + Locus(("10", "20")) - degenerate_equal(locus.to_coordinate, 9, [Point(position=0, offset=-1), Point(position=-1, offset=0)]) - degenerate_equal(locus.to_coordinate, 20, [Point(position=9, offset=1), Point(position=10, offset=0)]) + #Inverted Locus initialization + with pytest.raises(ValueError): + Locus((10, 5), inverted=True) + with pytest.raises(ValueError): + Locus((10, 20, 30), inverted=True) + with pytest.raises(ValueError): + Locus((10, -5), inverted=True) + with pytest.raises(ValueError): + Locus((10, 20.5), inverted=True) + with pytest.raises(ValueError): + Locus((10.5, None), inverted=True) + with pytest.raises(ValueError): + Locus(("10", "20"), inverted=True) -def test_Locus_inverted_degenerate(): - """Degenerate positions are silently corrected.""" - locus = Locus((10, 20), True) +def test_invalid_Coord_initialization(): + """Test Coord initialization.""" + with pytest.raises(ValueError): + Coord(-1) + with pytest.raises(ValueError): + Coord(3.5) + with pytest.raises(ValueError): + Coord("10") - degenerate_equal(locus.to_coordinate, 20, [Point(position=0, offset=-1), Point(position=-1, offset=0)]) - degenerate_equal(locus.to_coordinate, 9, [Point(position=9, offset=1), Point(position=10, offset=0)]) + +def test_Locus_invalid_point(): + """Forward orientent Locus with invalid point.""" + locus = Locus((30, 35)) + with pytest.raises(ValueError): + locus.to_coordinate(LocusPoint(position=-5, offset=0)) + with pytest.raises(IndexError): + locus.to_coordinate(LocusPoint(position=5, offset=0)) + with pytest.raises(IndexError): + locus.to_coordinate(LocusPoint(position=0, offset=2)) + with pytest.raises(IndexError): + locus.to_coordinate(LocusPoint(position=4, offset=-2)) + with pytest.raises(ValueError): + locus.to_coordinate(LocusPoint(position=2, offset=1)) + + + +def test_Locus_inverted_invalid_point(): + """Reverse orientent Locus with invalid point.""" + locus = Locus((30, 35), True) + with pytest.raises(ValueError): + locus.to_coordinate(LocusPoint(position=-5, offset=0)) + with pytest.raises(IndexError): + locus.to_coordinate(LocusPoint(position=5, offset=0)) + with pytest.raises(IndexError): + locus.to_coordinate(LocusPoint(position=0, offset=2)) + with pytest.raises(IndexError): + locus.to_coordinate(LocusPoint(position=4, offset=-2)) + with pytest.raises(ValueError): + locus.to_coordinate(LocusPoint(position=2, offset=1)) From 7aa63cc51114b450b2fade77a90991d0e821a9d3 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 14 Aug 2026 09:58:52 +0200 Subject: [PATCH 167/236] Add checks in multi_locus module. --- mutalyzer_crossmapper/multi_locus.py | 156 ++++++++++++++++----------- tests/test_multi_locus.py | 133 ++++++++--------------- 2 files changed, 136 insertions(+), 153 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 5b64d92..8a68da9 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -1,9 +1,22 @@ from bisect import bisect_right from itertools import accumulate -from operator import index +from dataclasses import dataclass + from .location import nearest_location -from .locus import Locus, Point +from .locus import Locus, Coord, Point as LocusPoint +from .checker import _check_exons + + +@dataclass(slots=True) +class Point(LocusPoint): + """Point dataclass""" + region: str = '' + + def __post_init__(self) -> None: + LocusPoint.__post_init__(self) + if self.region not in ('', 'u', 'd'): + raise ValueError(f"Region {self.region} is not valid. Must be '', 'u', or 'd'.") def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: @@ -20,57 +33,66 @@ def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: class MultiLocus(object): """MultiLocus object.""" - def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> None: + def __init__(self, locations: list[tuple[int, int]], length: None = None, inverted: bool = False) -> None: """ :arg list locations: List of locus locations. :arg bool inverted: Orientation. """ - # Check if the locations are non-overlapping , e.g., [(1, 5), (4, 10)] should be invalid; - # and sorted e.g., [(1, 5), (10, 15), (5, 10)] should be invalid. - # Look for circular chromosome sequence + _check_exons(locations) self._locations = locations self._inverted = inverted + self._end = sum(end - start for start, end in locations) + self._length = length self._loci = [Locus(location, inverted) for location in locations] self._orientation = -1 if inverted else 1 self._offsets = _offsets(locations, self._orientation) - # Consider add the length of the sequnce to the MultiLocus object, - # so that we can check if a coordinate is outside the sequence length. - def _validate_coordinate(self, coordinate: int) -> None: - """Check if a coordinate is within the MultiLocus. + def _validate_point(self, index: int, position: int, offset: int, region: str) -> None: + """Check if a point is valid under HGVS rules. - :arg int coordinate: Coordinate. + :arg int index: Index of the locus. - :raises ValueError: If coordinate is outside the MultiLocus. - """ - if coordinate < 0: - raise IndexError("Coordinate is outside sequence length.") + :arg int position: Position. - def _validate_point(self, index: int, point: Point) -> None: - """Check if a point is valid. + :arg int offset: Offset. - :arg int index: Index of the locus. - :arg Point point: Point model. + :arg str region: Region. - :raises ValueError: If point is outside the MultiLocus. + :raises ValueError: If point is outside the Locus. """ - if index == 0 and abs(point.offset) > self._loci[0].boundary[0]: - raise IndexError(f"Offset {point.offset} is outside the intron length {self._loci[0].boundary[0]}.") - if index > 0 and abs(point.offset) > self._loci[index].boundary[0] - self._loci[index - 1].boundary[1]: - raise IndexError(f"Offset {point.offset} is outside the intron length {self._loci[index].boundary[0] - self._loci[index - 1].boundary[1]}.") - - if point.offset < 0: - if point.position not in self._loci[index].boundary: - raise ValueError(f"Position {point.position} is not at an exon boundary.") - if self._loci[self._direction(index)].boundary[0] != point.position: - raise IndexError(f"Offset {point.offset} should be '-' when position is at exon start.") - - if point.offset > 0: - if point.position not in self._loci[index].boundary: - raise ValueError(f"Position {point.position} is not at an exon boundary.") - if self._loci[self._direction(index)].boundary[1] != point.position: - raise IndexError(f"Offset {point.offset} should be '+' when position is at exon end.") + # Upstream region validation, position is constant value and offset should not be positive + if region == 'u': + if position != self._offsets[0]: + raise ValueError(f"Position {position} is not at the upstream boundary.") + if offset > 0: + raise ValueError(f"Offset {offset} at upstream boundary should not be positive.") + if self._inverted: + if self._length is not None and abs(offset) >= self._length - self._loci[self._direction(0)].boundary[1]: + raise ValueError(f"Offset {offset} exceeds upstream region.") + else: + if abs(offset) >= self._loci[self._direction(0)].boundary[0]: + raise ValueError(f"Offset {offset} exceeds upstream boundary.") + # Downstream region validation, position is constant value and offset should not be negative + if region == 'd': + if position != self._end-1: + raise ValueError(f"Position {position} is not at the downstream boundary.") + if offset < 0: + raise ValueError(f"Offset {offset} at downstream boundary should not be negative.") + if not self._inverted: + if self._length is not None and abs(offset) >= self._length - self._loci[self._direction(-1)].boundary[1]: + raise ValueError(f"Offset {offset} exceeds downstream region.") + else: + if abs(offset) >= self._loci[self._direction(0)].boundary[0]: + raise ValueError(f"Offset {offset} exceeds downstream boundary.") + + if region == '': + if position > self._end: + raise IndexError(f"Position {position} exceeds MultiLocus length {self._end}") + if offset < 0 and abs(offset) > abs(self._loci[self._direction(index-1)].boundary[0] - self._loci[self._direction(index)].boundary[1]): + raise IndexError(f"Offset {offset} exceeds intron length.") + if offset > 0 and abs(offset) > abs(self._loci[self._direction(index+1)].boundary[0] - self._loci[self._direction(index)].boundary[1]): + raise IndexError(f"Offset {offset} exceeds intron length.") def _direction(self, index: int) -> int: @@ -78,7 +100,7 @@ def _direction(self, index: int) -> int: return len(self._offsets) - index - 1 return index - def outside(self, coordinate: int) -> int: + def _outside(self, coordinate: int) -> int: """Calculate the offset relative to this MultiLocus. :arg int coordinate: Coordinate. @@ -91,18 +113,18 @@ def outside(self, coordinate: int) -> int: return coordinate - self._loci[-1].boundary[1] return 0 - def to_position(self, coordinate: int) -> Point: + def to_position(self, coord: Coord) -> Point: """Convert a coordinate to a point model. - :arg int coordinate: Coordinate. + :arg Coord coord: Coordinate model. - :returns Point: Point model . + :returns Point: Point model. """ - self._validate_coordinate(coordinate) - index = nearest_location(self._locations, coordinate, self._inverted) - outside = self._orientation * self.outside(coordinate) + index = nearest_location(self._locations, coord.coordinate, self._inverted) + outside = self._orientation * self._outside(coord.coordinate) region = 'u' if outside < 0 else 'd' if outside > 0 else '' - point = self._loci[index].to_position(coordinate) + point = self._loci[index].to_position(coord) + return Point( position=point.position + self._offsets[self._direction(index)], offset=point.offset, @@ -116,27 +138,37 @@ def to_coordinate(self, point: Point) -> int: :returns int: Coordinate. """ - + index = min( + len(self._offsets), + max(0, bisect_right(self._offsets, point.position) - 1) + ) + self._validate_point(index, point.position, point.offset, point.region) if point.region == 'u': if self._inverted: - return self._locations[-1][1] - point.offset - 1 - self._validate_point(0, point) - return self._locations[0][0] + point.offset + return Coord(self._locations[-1][1] - point.offset - 1) + return Coord(self._locations[0][0] + point.offset) if point.region == 'd': if self._inverted: - return self._locations[0][0] - point.offset - return self._locations[-1][1] + point.offset - 1 - - index = min( - len(self._offsets), - max(0, bisect_right(self._offsets, point.position) - 1) - ) - self._validate_point(index, point) - return self._loci[self._direction(index)].to_coordinate( - Point( - position=point.position - self._offsets[index], - offset=point.offset, - region=point.region, + return Coord(self._locations[0][0] - point.offset) + return Coord(self._locations[-1][1] + point.offset - 1) + + try: + return self._loci[self._direction(index)].to_coordinate( + Point( + position=point.position - self._offsets[index], + offset=point.offset, + ) ) - ) + + except ValueError as e: + if "Position" in str(e): + raise ValueError(f"Position {point.position} is not at a locus boundary.") from e + raise e + except IndexError as e: + if "Position" in str(e): + raise IndexError( + f"Position {point.position} exceeds locus length {self._loci[self._direction(index)].boundary[1] - self._loci[self._direction(index)].boundary[0]}" + ) from e + raise e + diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index f076e81..307d243 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -1,8 +1,7 @@ -from mutalyzer_crossmapper import MultiLocus -from mutalyzer_crossmapper.multi_locus import _offsets -from mutalyzer_crossmapper.locus import Point +from mutalyzer_crossmapper.multi_locus import _offsets, Point, Coord, MultiLocus +# from mutalyzer_crossmapper.locus import Point -from helper import degenerate_equal, invariant +from helper import invariant _locations = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] @@ -34,14 +33,14 @@ def test_MultiLocus(): # Boundary between upstream and the first locus. invariant( multi_locus.to_position, - 4, + Coord(4), multi_locus.to_coordinate, Point(position=0, offset=-1, region='u'), ) invariant( multi_locus.to_position, - 5, + Coord(5), multi_locus.to_coordinate, Point(position=0, offset=0, region=''), ) @@ -49,37 +48,37 @@ def test_MultiLocus(): # Internal locus. invariant( multi_locus.to_position, - 29, + Coord(29), multi_locus.to_coordinate, Point(position=9, offset=-1, region=''), ) invariant( multi_locus.to_position, - 30, + Coord(30), multi_locus.to_coordinate, Point(position=9, offset=0, region=''), ) invariant( multi_locus.to_position, - 31, + Coord(31), multi_locus.to_coordinate, Point(position=10, offset=0, region=''), ) invariant( multi_locus.to_position, - 33, + Coord(33), multi_locus.to_coordinate, Point(position=12, offset=0, region=''), ) invariant( multi_locus.to_position, - 34, + Coord(34), multi_locus.to_coordinate, Point(position=13, offset=0, region=''), ) invariant( multi_locus.to_position, - 35, + Coord(35), multi_locus.to_coordinate, Point(position=13, offset=1, region=''), ) @@ -87,13 +86,13 @@ def test_MultiLocus(): # Boundary between the last locus and downstream. invariant( multi_locus.to_position, - 71, + Coord(71), multi_locus.to_coordinate, Point(position=21, offset=0, region=''), ) invariant( multi_locus.to_position, - 72, + Coord(72), multi_locus.to_coordinate, Point(position=21, offset=1, region='d'), ) @@ -101,18 +100,18 @@ def test_MultiLocus(): def test_MultiLocus_inverted(): """Reverse oriented MultiLocus.""" - multi_locus = MultiLocus(_locations, True) + multi_locus = MultiLocus(_locations, None, True) # Boundary between upstream and the first locus. invariant( multi_locus.to_position, - 72, + Coord(72), multi_locus.to_coordinate, Point(position=0, offset=-1, region='u'), ) invariant( multi_locus.to_position, - 71, + Coord(71), multi_locus.to_coordinate, Point(position=0, offset=0, region=''), ) @@ -120,37 +119,37 @@ def test_MultiLocus_inverted(): # Internal locus. invariant( multi_locus.to_position, - 35, + Coord(35), multi_locus.to_coordinate, Point(position=8, offset=-1, region=''), ) invariant( multi_locus.to_position, - 34, + Coord(34), multi_locus.to_coordinate, Point(position=8, offset=0, region=''), ) invariant( multi_locus.to_position, - 33, + Coord(33), multi_locus.to_coordinate, Point(position=9, offset=0, region=''), ) invariant( multi_locus.to_position, - 31, + Coord(31), multi_locus.to_coordinate, Point(position=11, offset=0, region=''), ) invariant( multi_locus.to_position, - 30, + Coord(30), multi_locus.to_coordinate, Point(position=12, offset=0, region=''), ) invariant( multi_locus.to_position, - 29, + Coord(29), multi_locus.to_coordinate, Point(position=12, offset=1, region=''), ) @@ -158,13 +157,13 @@ def test_MultiLocus_inverted(): # Boundary between the last locus and downstream. invariant( multi_locus.to_position, - 5, + Coord(5), multi_locus.to_coordinate, Point(position=21, offset=0, region=''), ) invariant( multi_locus.to_position, - 4, + Coord(4), multi_locus.to_coordinate, Point(position=21, offset=1, region='d'), ) @@ -176,13 +175,13 @@ def test_MultiLocus_adjacent_loci(): invariant( multi_locus.to_position, - 2, + Coord(2), multi_locus.to_coordinate, Point(position=1, offset=0, region=''), ) invariant( multi_locus.to_position, - 3, + Coord(3), multi_locus.to_coordinate, Point(position=2, offset=0, region=''), ) @@ -190,17 +189,17 @@ def test_MultiLocus_adjacent_loci(): def test_MultiLocus_adjacent_loci_inverted(): """Positions are continuous when loci are adjacent.""" - multi_locus = MultiLocus([(1, 3), (3, 5)], True) + multi_locus = MultiLocus([(1, 3), (3, 5)], None, True) invariant( multi_locus.to_position, - 3, + Coord(3), multi_locus.to_coordinate, Point(position=1, offset=0, region=''), ) invariant( multi_locus.to_position, - 2, + Coord(2), multi_locus.to_coordinate, Point(position=2, offset=0, region=''), ) @@ -212,13 +211,13 @@ def test_MultiLocus_offsets_odd(): invariant( multi_locus.to_position, - 4, + Coord(4), multi_locus.to_coordinate, Point(position=1, offset=2, region=''), ) invariant( multi_locus.to_position, - 5, + Coord(5), multi_locus.to_coordinate, Point(position=2, offset=-1, region=''), ) @@ -226,17 +225,18 @@ def test_MultiLocus_offsets_odd(): def test_MultiLocus_offsets_odd_inverted(): """Offets exacly between two loci are assigned to the upstream locus.""" - multi_locus = MultiLocus([(1, 3), (6, 8)], True) - + multi_locus = MultiLocus([(1, 3), (6, 8)], None, True) + print(multi_locus.to_position(Coord(4))) + print(multi_locus.to_position(Coord(3))) invariant( multi_locus.to_position, - 4, + Coord(4), multi_locus.to_coordinate, Point(position=1, offset=2, region=''), ) invariant( multi_locus.to_position, - 3, + Coord(3), multi_locus.to_coordinate, Point(position=2, offset=-1, region=''), ) @@ -248,13 +248,13 @@ def test_MultiLocus_offsets_even(): invariant( multi_locus.to_position, - 4, + Coord(4), multi_locus.to_coordinate, Point(position=1, offset=2, region=''), ) invariant( multi_locus.to_position, - 5, + Coord(5), multi_locus.to_coordinate, Point(position=2, offset=-2, region=''), ) @@ -262,66 +262,17 @@ def test_MultiLocus_offsets_even(): def test_MultiLocus_offsets_even_inverted(): """Offsets are assigned to the nearest locus.""" - multi_locus = MultiLocus([(1, 3), (7, 9)], True) + multi_locus = MultiLocus([(1, 3), (7, 9)], None, True) invariant( multi_locus.to_position, - 5, + Coord(5), multi_locus.to_coordinate, Point(position=1, offset=2, region=''), ) invariant( multi_locus.to_position, - 4, + Coord(4), multi_locus.to_coordinate, Point(position=2, offset=-2, region=''), - ) - - -def test_MultiLocus_degenerate(): - """Degenerate upstream and downstream positions are silently corrected.""" - multi_locus = MultiLocus(_locations) - - degenerate_equal( - multi_locus.to_coordinate, - 4, - [ - Point(position=0, offset=-1, region='u'), - Point(position=-1, offset=0, region=''), - ], - ) - - degenerate_equal( - multi_locus.to_coordinate, - 72, - [ - Point(position=21, offset=1, region='d'), - Point(position=22, offset=0, region=''), - Point(position=22, offset=1, region='d'), - ], - ) - - -def test_MultiLocus_inverted_degenerate(): - """Degenerate upstream and downstream positions are silently corrected.""" - multi_locus = MultiLocus(_locations, True) - - degenerate_equal( - multi_locus.to_coordinate, - 72, - [ - Point(position=-1, offset=0, region=''), - Point(position=0, offset=-1, region=''), - Point(position=0, offset=-1, region='u'), - ], - ) - - degenerate_equal( - multi_locus.to_coordinate, - 4, - [ - Point(position=21, offset=1, region=''), - Point(position=22, offset=0, region=''), - Point(position=21, offset=1, region='d'), - ], - ) + ) \ No newline at end of file From b0f40c603c4e2db779fbbd710b1580c63bebe851 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 18 Aug 2026 12:39:13 +0200 Subject: [PATCH 168/236] Move checks from checker to locus module. --- mutalyzer_crossmapper/locus.py | 34 +++++++++++-- tests/test_locus.py | 90 ++++++++++++++++++---------------- 2 files changed, 77 insertions(+), 47 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 24af131..7ad5afd 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from .checker import _check_locus, _check_non_negative, _check_int @dataclass(slots=True) @@ -9,7 +8,7 @@ class Point: offset: int = 0 def __post_init__(self) -> None: - _check_non_negative(self.position) + _check_non_negative_int(self.position) _check_int(self.offset) @@ -19,7 +18,32 @@ class Coord: coordinate: int def __post_init__(self) -> None: - _check_non_negative(self.coordinate) + _check_non_negative_int(self.coordinate) + + +def _check_int(value: int) -> None: + """Check if the value type is integer.""" + if not isinstance(value, int): + raise ValueError("Value must be an integer.") + + +def _check_non_negative_int(value: int) -> None: + """Check if the coordinate is a non-negative integer.""" + _check_int(value) + if value < 0: + raise ValueError("Value must be non-negative.") + + +def _check_locus(locus: tuple[int, int]) -> None: + """Check if the range is valid.""" + if len(locus) != 2: + raise ValueError("Locus must be a tuple of two values.") + + for value in locus: + _check_non_negative_int(value) + + if locus[0] >= locus[1]: + raise ValueError(f"Locus start {locus[0]} must be smaller than locus end {locus[1]}.") class Locus(object): @@ -33,7 +57,7 @@ def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: _check_locus(location) self._inverted = inverted - self.boundary = location[0], location[1] - 1 #: 0 based open on coordinate + self.boundary = location[0], location[1] - 1 self._end = self.boundary[1] - self.boundary[0] def _validate_point(self, position, offset) -> None: @@ -48,7 +72,7 @@ def _validate_point(self, position, offset) -> None: raise IndexError(f"Offset {offset} should be at a locus start.") if offset > 0 and position != self._end: raise IndexError(f"Offset {offset} should be at a locus end.") - if position > self._end: + if position > max(self._end, 0): raise IndexError(f"Position {position} exceeds locus length {self._end + 1}") def to_position(self, coord: Coord) -> Point: diff --git a/tests/test_locus.py b/tests/test_locus.py index 89419cc..894f536 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -1,36 +1,9 @@ -from mutalyzer_crossmapper import Locus -from mutalyzer_crossmapper.locus import Point, Coord +from mutalyzer_crossmapper.locus import Locus, Point, Coord -from helper import degenerate_equal, invariant +from helper import invariant import pytest -def test_Locus(): - """Forward orientent Locus.""" - locus = Locus((30, 35)) - - invariant(locus.to_position, Coord(29), locus.to_coordinate, Point(position=0, offset=-1)) - invariant(locus.to_position, Coord(30), locus.to_coordinate, Point(position=0, offset=0)) - invariant(locus.to_position, Coord(31), locus.to_coordinate, Point(position=1, offset=0)) - invariant(locus.to_position, Coord(33), locus.to_coordinate, Point(position=3, offset=0)) - invariant(locus.to_position, Coord(34), locus.to_coordinate, Point(position=4, offset=0)) - invariant(locus.to_position, Coord(35), locus.to_coordinate, Point(position=4, offset=1)) - - -def test_Locus_inverted(): - """Reverse orientent Locus.""" - locus = Locus((30, 35), True) - - invariant(locus.to_position, Coord(35), locus.to_coordinate, Point(position=0, offset=-1)) - invariant(locus.to_position, Coord(34), locus.to_coordinate, Point(position=0, offset=0)) - invariant(locus.to_position, Coord(33), locus.to_coordinate, Point(position=1, offset=0)) - invariant(locus.to_position, Coord(31), locus.to_coordinate, Point(position=3, offset=0)) - invariant(locus.to_position, Coord(30), locus.to_coordinate, Point(position=4, offset=0)) - invariant(locus.to_position, Coord(29), locus.to_coordinate, Point(position=4, offset=1)) - -from mutalyzer_crossmapper.locus import Locus, Coord, Point as LocusPoint - -## Test Locus model and its point model def test_invalid_Locus_initialization(): """Test Locus initialization.""" with pytest.raises(ValueError): @@ -45,6 +18,8 @@ def test_invalid_Locus_initialization(): Locus((10.5, None)) with pytest.raises(ValueError): Locus(("10", "20")) + with pytest.raises(ValueError): + Locus((10, 10)) #Inverted Locus initialization with pytest.raises(ValueError): @@ -59,6 +34,8 @@ def test_invalid_Locus_initialization(): Locus((10.5, None), inverted=True) with pytest.raises(ValueError): Locus(("10", "20"), inverted=True) + with pytest.raises(ValueError): + Locus((10, 10), inverted=True) def test_invalid_Coord_initialization(): @@ -69,34 +46,63 @@ def test_invalid_Coord_initialization(): Coord(3.5) with pytest.raises(ValueError): Coord("10") + with pytest.raises(ValueError): + Coord(None) + with pytest.raises(ValueError): + Coord([10]) -def test_Locus_invalid_point(): +def test_invalid_Locus_point(): """Forward orientent Locus with invalid point.""" locus = Locus((30, 35)) with pytest.raises(ValueError): - locus.to_coordinate(LocusPoint(position=-5, offset=0)) + locus.to_coordinate(Point(position=-5, offset=0)) with pytest.raises(IndexError): - locus.to_coordinate(LocusPoint(position=5, offset=0)) + locus.to_coordinate(Point(position=5, offset=0)) with pytest.raises(IndexError): - locus.to_coordinate(LocusPoint(position=0, offset=2)) + locus.to_coordinate(Point(position=0, offset=2)) with pytest.raises(IndexError): - locus.to_coordinate(LocusPoint(position=4, offset=-2)) + locus.to_coordinate(Point(position=4, offset=-2)) with pytest.raises(ValueError): - locus.to_coordinate(LocusPoint(position=2, offset=1)) - + locus.to_coordinate(Point(position=2, offset=1)) -def test_Locus_inverted_invalid_point(): +def test_invalid_Locus_inverted_point(): """Reverse orientent Locus with invalid point.""" locus = Locus((30, 35), True) with pytest.raises(ValueError): - locus.to_coordinate(LocusPoint(position=-5, offset=0)) + locus.to_coordinate(Point(position=-5, offset=0)) with pytest.raises(IndexError): - locus.to_coordinate(LocusPoint(position=5, offset=0)) + locus.to_coordinate(Point(position=5, offset=0)) with pytest.raises(IndexError): - locus.to_coordinate(LocusPoint(position=0, offset=2)) + locus.to_coordinate(Point(position=0, offset=2)) with pytest.raises(IndexError): - locus.to_coordinate(LocusPoint(position=4, offset=-2)) + locus.to_coordinate(Point(position=4, offset=-2)) with pytest.raises(ValueError): - locus.to_coordinate(LocusPoint(position=2, offset=1)) + locus.to_coordinate(Point(position=2, offset=1)) + + +def test_Locus(): + """Forward orientent Locus.""" + locus = Locus((30, 35)) + print(locus.to_coordinate(Point(position=4, offset=1))) + print(locus.to_position(Coord(35))) + + invariant(locus.to_position, Coord(29), locus.to_coordinate, Point(position=0, offset=-1)) + invariant(locus.to_position, Coord(30), locus.to_coordinate, Point(position=0, offset=0)) + invariant(locus.to_position, Coord(31), locus.to_coordinate, Point(position=1, offset=0)) + invariant(locus.to_position, Coord(33), locus.to_coordinate, Point(position=3, offset=0)) + invariant(locus.to_position, Coord(34), locus.to_coordinate, Point(position=4, offset=0)) + invariant(locus.to_position, Coord(35), locus.to_coordinate, Point(position=4, offset=1)) + + +def test_Locus_inverted(): + """Reverse orientent Locus.""" + locus = Locus((30, 35), True) + + invariant(locus.to_position, Coord(35), locus.to_coordinate, Point(position=0, offset=-1)) + invariant(locus.to_position, Coord(34), locus.to_coordinate, Point(position=0, offset=0)) + invariant(locus.to_position, Coord(33), locus.to_coordinate, Point(position=1, offset=0)) + invariant(locus.to_position, Coord(31), locus.to_coordinate, Point(position=3, offset=0)) + invariant(locus.to_position, Coord(30), locus.to_coordinate, Point(position=4, offset=0)) + invariant(locus.to_position, Coord(29), locus.to_coordinate, Point(position=4, offset=1)) From af7f7f71ae002ad3f2f177cba63d8d6ffa72494e Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 18 Aug 2026 14:48:12 +0200 Subject: [PATCH 169/236] Cleanup. --- tests/test_locus.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_locus.py b/tests/test_locus.py index 894f536..9656a25 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -85,8 +85,6 @@ def test_invalid_Locus_inverted_point(): def test_Locus(): """Forward orientent Locus.""" locus = Locus((30, 35)) - print(locus.to_coordinate(Point(position=4, offset=1))) - print(locus.to_position(Coord(35))) invariant(locus.to_position, Coord(29), locus.to_coordinate, Point(position=0, offset=-1)) invariant(locus.to_position, Coord(30), locus.to_coordinate, Point(position=0, offset=0)) From 315c9e307e5984e77b22bc88329eda89e3a4fde5 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 18 Aug 2026 14:49:42 +0200 Subject: [PATCH 170/236] Delete checker module and place checks parsely. --- mutalyzer_crossmapper/checker.py | 84 -------------------------------- 1 file changed, 84 deletions(-) delete mode 100644 mutalyzer_crossmapper/checker.py diff --git a/mutalyzer_crossmapper/checker.py b/mutalyzer_crossmapper/checker.py deleted file mode 100644 index d82ef97..0000000 --- a/mutalyzer_crossmapper/checker.py +++ /dev/null @@ -1,84 +0,0 @@ -from mutalyzer_crossmapper.location import nearest_location - - -def _check_int(value: int) -> None: - """Check if the value is a non-negative integer. - - :arg int value: Value to check. - - :raises ValueError: If the value is invalid. - """ - if not isinstance(value, int): - raise ValueError("Value must be an integer.") - - -def _check_in_range(value: int, length: int) -> None: - if value > length: - raise ValueError(f"Value {value} must be within the bounds of the reference sequence {length}.") - - -def _check_non_negative(value: int, length: int|None = None) -> None: - """Check if the coordinate is a non-negative integer. - - :arg int value: Value to check. - - :raises ValueError: If the coordinate is invalid. - """ - _check_int(value) - if value < 0: - raise ValueError("Value must be non-negative.") - if length is not None: - _check_in_range(value, length) - - -def _check_locus(locus: tuple[int, int], length: int| None = None) -> None: - """Check if the range is valid. - - :arg tuple[int, int] locus: Locus to check. - - :raises ValueError: If the range is invalid. - """ - if len(locus) != 2: - raise ValueError("Locus must be a tuple of two values.") - - for value in locus: - _check_non_negative(value, length) - - if locus[0] > locus[1]: - raise ValueError("Start of locus must be smaller than or equal to end of locus.") - - -def _check_exons(exons: list[tuple[int, int]], length: int|None = None) -> None: - """Check if the exons are valid. - The exons are valid - if they are a list of valid loci, - non-overlapping, - and within the bounds of the reference sequence. - - :arg list[tuple[int, int]] exons: Exons to check. - - :raises ValueError: If the exons are invalid. - """ - for exon in exons: - _check_locus(exon, length) - - for e1, e2 in zip(exons, exons[1:]): - if e2[0] < e1[1]: - raise ValueError(f"Exon {e2} and exon {e1} are overlapping.") - - -def _check_cds(cds: tuple[int, int], exons: list[tuple[int, int]], length: int|None = None) -> None: - """Check if the CDS is valid. - - :arg tuple[int, int] cds: CDS to check. - :arg list[tuple[int, int]] exons: List of exons. - :arg int|None length: Length of the reference sequence. - - :raises ValueError: If the CDS is invalid. - """ - _check_locus(cds, length) - for coord in cds: - index = nearest_location(exons, coord) - if coord < exons[index][0] or coord >= exons[index][1]: - raise ValueError(f"Coordinate {coord} of CDS {cds} is not within any exon.") - From 692b9bd7336852c09c1d3618fc94a0dc29b43a1d Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 18 Aug 2026 15:02:37 +0200 Subject: [PATCH 171/236] Change locus length to one based. --- mutalyzer_crossmapper/locus.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 7ad5afd..ade1fe0 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -58,7 +58,7 @@ def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: self._inverted = inverted self.boundary = location[0], location[1] - 1 - self._end = self.boundary[1] - self.boundary[0] + self._end = location[1] - location[0] # one-based length of the locus def _validate_point(self, position, offset) -> None: """Validate a point model under HGVS rules. @@ -66,14 +66,14 @@ def _validate_point(self, position, offset) -> None: :arg int position: Position. :arg int offset: Offset. """ - if offset != 0 and position not in (0, self._end): + if offset != 0 and position not in (0, self._end-1): raise ValueError(f"Position {position} is not at locus boundary.") if offset < 0 and position != 0: raise IndexError(f"Offset {offset} should be at a locus start.") - if offset > 0 and position != self._end: + if offset > 0 and position != self._end-1: raise IndexError(f"Offset {offset} should be at a locus end.") - if position > max(self._end, 0): - raise IndexError(f"Position {position} exceeds locus length {self._end + 1}") + if position > self._end-1: + raise IndexError(f"Position {position} exceeds locus length {self._end}") def to_position(self, coord: Coord) -> Point: """Convert a coordinate to a proper point model. @@ -86,13 +86,13 @@ def to_position(self, coord: Coord) -> Point: if coord.coordinate > self.boundary[1]: return Point(position=0, offset=self.boundary[1] - coord.coordinate) if coord.coordinate < self.boundary[0]: - return Point(position=self._end, offset=self.boundary[0] - coord.coordinate) + return Point(position=self._end-1, offset=self.boundary[0] - coord.coordinate) return Point(position=self.boundary[1] - coord.coordinate, offset=0) if coord.coordinate < self.boundary[0]: return Point(position=0, offset=coord.coordinate - self.boundary[0]) if coord.coordinate > self.boundary[1]: - return Point(position=self._end, offset=coord.coordinate - self.boundary[1]) + return Point(position=self._end-1, offset=coord.coordinate - self.boundary[1]) return Point(position=coord.coordinate - self.boundary[0], offset=0) def to_coordinate(self, point: Point) -> Coord: From c43212900be5c9c8f725c62bcbe6ec052414f7c0 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 18 Aug 2026 16:17:43 +0200 Subject: [PATCH 172/236] Add checks in multi_locus module and its tests. --- mutalyzer_crossmapper/multi_locus.py | 54 +++++-- tests/test_multi_locus.py | 228 ++++++++++++++++++++++++++- 2 files changed, 261 insertions(+), 21 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 8a68da9..b3655c6 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -4,8 +4,7 @@ from .location import nearest_location -from .locus import Locus, Coord, Point as LocusPoint -from .checker import _check_exons +from .locus import Locus, Coord, Point as LocusPoint, _check_locus @dataclass(slots=True) @@ -19,6 +18,26 @@ def __post_init__(self) -> None: raise ValueError(f"Region {self.region} is not valid. Must be '', 'u', or 'd'.") +def _check_in_range(value: int, length: int | None = None) -> None: + """Check if the value no larger than length.""" + print(value, length) + if length is not None and value >= length: + raise ValueError(f"Value {value} must be within the bounds of the reference sequence {length}.") + + +def _check_multi_locus(locations: list[tuple[int, int]], length: int | None = None) -> None: + print(locations, length) + """Check if the locations list is valid.""" + for locus in locations: + _check_locus(locus) + + for l1, l2 in zip(locations, locations[1:]): + if l2[0] < l1[1]: + raise ValueError(f"Locus {l2} and locus {l1} are overlapping.") + + _check_in_range(locations[-1][1], length) + + def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: """For each location, calculate the length of the preceding locations. @@ -33,33 +52,33 @@ def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: class MultiLocus(object): """MultiLocus object.""" - def __init__(self, locations: list[tuple[int, int]], length: None = None, inverted: bool = False) -> None: + def __init__(self, locations: list[tuple[int, int]], length: int |None = None, inverted: bool = False) -> None: """ :arg list locations: List of locus locations. :arg bool inverted: Orientation. """ - _check_exons(locations) + _check_multi_locus(locations, length) self._locations = locations self._inverted = inverted - self._end = sum(end - start for start, end in locations) self._length = length self._loci = [Locus(location, inverted) for location in locations] self._orientation = -1 if inverted else 1 self._offsets = _offsets(locations, self._orientation) + self._end = sum(end - start for start, end in locations) # one-based length of the MultiLocus + + def _validate_coord(self, coord) -> None: + """Check if the coordinate is valid.""" + if self._length is not None: + _check_in_range(coord, self._length) def _validate_point(self, index: int, position: int, offset: int, region: str) -> None: """Check if a point is valid under HGVS rules. :arg int index: Index of the locus. - :arg int position: Position. - :arg int offset: Offset. - :arg str region: Region. - - :raises ValueError: If point is outside the Locus. """ # Upstream region validation, position is constant value and offset should not be positive if region == 'u': @@ -71,25 +90,27 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - if self._length is not None and abs(offset) >= self._length - self._loci[self._direction(0)].boundary[1]: raise ValueError(f"Offset {offset} exceeds upstream region.") else: - if abs(offset) >= self._loci[self._direction(0)].boundary[0]: + if abs(offset) > self._loci[self._direction(0)].boundary[0]: raise ValueError(f"Offset {offset} exceeds upstream boundary.") + # Downstream region validation, position is constant value and offset should not be negative if region == 'd': if position != self._end-1: - raise ValueError(f"Position {position} is not at the downstream boundary.") + raise ValueError(f"Position {position} is not at the downstream boundary {self._end-1}.") if offset < 0: raise ValueError(f"Offset {offset} at downstream boundary should not be negative.") if not self._inverted: if self._length is not None and abs(offset) >= self._length - self._loci[self._direction(-1)].boundary[1]: raise ValueError(f"Offset {offset} exceeds downstream region.") else: - if abs(offset) >= self._loci[self._direction(0)].boundary[0]: + if abs(offset) > self._loci[self._direction(0)].boundary[0]: raise ValueError(f"Offset {offset} exceeds downstream boundary.") + # '' region validation, position should be within the MultiLocus and offset should not exceed intron length if region == '': - if position > self._end: + if position > self._end-1: raise IndexError(f"Position {position} exceeds MultiLocus length {self._end}") - if offset < 0 and abs(offset) > abs(self._loci[self._direction(index-1)].boundary[0] - self._loci[self._direction(index)].boundary[1]): + if offset < 0 and abs(offset) > abs(self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index-1)].boundary[1]): raise IndexError(f"Offset {offset} exceeds intron length.") if offset > 0 and abs(offset) > abs(self._loci[self._direction(index+1)].boundary[0] - self._loci[self._direction(index)].boundary[1]): raise IndexError(f"Offset {offset} exceeds intron length.") @@ -120,6 +141,7 @@ def to_position(self, coord: Coord) -> Point: :returns Point: Point model. """ + self._validate_coord(coord.coordinate) index = nearest_location(self._locations, coord.coordinate, self._inverted) outside = self._orientation * self._outside(coord.coordinate) region = 'u' if outside < 0 else 'd' if outside > 0 else '' @@ -168,7 +190,7 @@ def to_coordinate(self, point: Point) -> int: except IndexError as e: if "Position" in str(e): raise IndexError( - f"Position {point.position} exceeds locus length {self._loci[self._direction(index)].boundary[1] - self._loci[self._direction(index)].boundary[0]}" + f"Position {point.position} exceeds locus length {self._offsets[self._direction(index)] + self._loci[self._direction(index)].boundary[1] - self._loci[self._direction(index)].boundary[0]}" ) from e raise e diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index 307d243..d22d414 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -1,8 +1,8 @@ -from mutalyzer_crossmapper.multi_locus import _offsets, Point, Coord, MultiLocus -# from mutalyzer_crossmapper.locus import Point - +from mutalyzer_crossmapper.multi_locus import _offsets, Coord, MultiLocus, Point from helper import invariant +import pytest + _locations = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] @@ -26,6 +26,84 @@ def test_offsets_adjacent_inverted(): assert _offsets([(1, 3), (3, 5)], -1) == [0, 2] +## Test MultiLocus model and its point model +def test_invalid_MultiLocus_initialization(): + """Test MultiLocus initialization.""" + with pytest.raises(ValueError): + MultiLocus(([(10, 5), (20, 25)])) + with pytest.raises(ValueError): + MultiLocus([(10, 20, 30), (40, 50)]) + with pytest.raises(ValueError): + MultiLocus([(10, -5), (20, 25)]) + with pytest.raises(ValueError): + MultiLocus([(10, 20.5), (30, 40)]) + with pytest.raises(ValueError): + MultiLocus([(10.5, None), (20, 30)]) + with pytest.raises(ValueError): + MultiLocus([("10", "20"), (30, 40)]) + with pytest.raises(ValueError): + MultiLocus([(10, 20), (15, 25)]) + with pytest.raises(ValueError): + MultiLocus([(10, 12), (15, 25)], 25) + + # Inverted MultiLocus initialization + with pytest.raises(ValueError): + MultiLocus(([(10, 5), (20, 25)]), inverted=True) + with pytest.raises(ValueError): + MultiLocus([(10, 20, 30), (40, 50)], inverted=True) + with pytest.raises(ValueError): + MultiLocus([(10, -5), (20, 25)], inverted=True) + with pytest.raises(ValueError): + MultiLocus([(10, 20.5), (30, 40)], inverted=True) + with pytest.raises(ValueError): + MultiLocus([(10.5, None), (20, 30)], inverted=True) + with pytest.raises(ValueError): + MultiLocus([("10", "20"), (30, 40)], inverted=True) + with pytest.raises(ValueError): + MultiLocus([(10, 20), (15, 25)], 25, inverted=True) + with pytest.raises(ValueError): + MultiLocus([(10, 12), (15, 25)], 25, inverted=True) + + +def test_MultiLocus_invalid_coordinate(): + """Forward orientent MultiLocus with invalid coordinate.""" + multi_locus = MultiLocus([(30, 35), (40, 45)]) + with pytest.raises(ValueError): + multi_locus.to_position(Coord(-1)) + with pytest.raises(ValueError): + multi_locus.to_position(Coord(46.7)) + with pytest.raises(ValueError): + multi_locus.to_position(Coord("31")) + + +def test_MultiLocus_invalid_point(): + """Forward orientent MultiLocus with invalid point.""" + multi_locus = MultiLocus([(5, 10), (15, 20)]) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=-5, offset=0, region='')) + with pytest.raises(IndexError): + multi_locus.to_coordinate(Point(position=0, offset=2, region='')) + with pytest.raises(IndexError): + multi_locus.to_coordinate(Point(position=4, offset=-2, region='')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=2, offset=1, region='')) + + +def test_MultiLocus_inverted_invalid_point(): + """Reverse orientent MultiLocus with invalid point.""" + multi_locus = MultiLocus([(30, 35), (40, 45)], inverted=True) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=-5, offset=0, region='')) + with pytest.raises(IndexError): + multi_locus.to_coordinate(Point(position=5, offset=1, region='')) + with pytest.raises(IndexError): + multi_locus.to_coordinate(Point(position=0, offset=2, region='')) + with pytest.raises(IndexError): + multi_locus.to_coordinate(Point(position=4, offset=-2, region='')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=2, offset=1, region='')) + + def test_MultiLocus(): """Forward oriented MultiLocus.""" multi_locus = MultiLocus(_locations) @@ -169,6 +247,66 @@ def test_MultiLocus_inverted(): ) +def test_MultiLocus_with_length(): + """Forward oriented MultiLocus.""" + multi_locus = MultiLocus(_locations, length=74) + + # Boundary between the last locus and downstream. + invariant( + multi_locus.to_position, + Coord(71), + multi_locus.to_coordinate, + Point(position=21, offset=0, region=''), + ) + invariant( + multi_locus.to_position, + Coord(72), + multi_locus.to_coordinate, + Point(position=21, offset=1, region='d'), + ) + invariant( + multi_locus.to_position, + Coord(73), + multi_locus.to_coordinate, + Point(position=21, offset=2, region='d'), + ) + # Boundary between the last base and beyond the last base. + with pytest.raises(ValueError): + multi_locus.to_position(Coord(74)) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=21, offset=3, region='d')) + + +def test_MultiLocus_inverted_with_length(): + """Inverted MultiLocus with length.""" + multi_locus = MultiLocus(_locations, length=74, inverted=True) + + # Boundary between the first locus and upstream. + invariant( + multi_locus.to_position, + Coord(71), + multi_locus.to_coordinate, + Point(position=0, offset=0, region=''), + ) + invariant( + multi_locus.to_position, + Coord(72), + multi_locus.to_coordinate, + Point(position=0, offset=-1, region='u'), + ) + # Boundary between the first base beyond the first base. + invariant( + multi_locus.to_position, + Coord(73), + multi_locus.to_coordinate, + Point(position=0, offset=-2, region='u'), + ) + with pytest.raises(ValueError): + multi_locus.to_position(Coord(74)) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=0, offset=-3, region='u')) + + def test_MultiLocus_adjacent_loci(): """Positions are continuous when loci are adjacent.""" multi_locus = MultiLocus([(1, 3), (3, 5)]) @@ -226,8 +364,6 @@ def test_MultiLocus_offsets_odd(): def test_MultiLocus_offsets_odd_inverted(): """Offets exacly between two loci are assigned to the upstream locus.""" multi_locus = MultiLocus([(1, 3), (6, 8)], None, True) - print(multi_locus.to_position(Coord(4))) - print(multi_locus.to_position(Coord(3))) invariant( multi_locus.to_position, Coord(4), @@ -275,4 +411,86 @@ def test_MultiLocus_offsets_even_inverted(): Coord(4), multi_locus.to_coordinate, Point(position=2, offset=-2, region=''), + ) + + +def test_one_base_exon(): + """One base exons.""" + multi_locus = MultiLocus([(1, 2), (4, 5)]) + invariant( + multi_locus.to_position, + Coord(0), + multi_locus.to_coordinate, + Point(position=0, offset=-1, region='u'), + ) + invariant( + multi_locus.to_position, + Coord(1), + multi_locus.to_coordinate, + Point(position=0, offset=0, region=''), + ) + invariant( + multi_locus.to_position, + Coord(2), + multi_locus.to_coordinate, + Point(position=0, offset=1, region=''), + ) + invariant( + multi_locus.to_position, + Coord(3), + multi_locus.to_coordinate, + Point(position=1, offset=-1, region=''), + ) + invariant( + multi_locus.to_position, + Coord(4), + multi_locus.to_coordinate, + Point(position=1, offset=0, region=''), + ) + invariant( + multi_locus.to_position, + Coord(5), + multi_locus.to_coordinate, + Point(position=1, offset=1, region='d'), + ) + + +def test_one_base_exon_inverted(): + """One base exons.""" + multi_locus = MultiLocus([(1, 2), (4, 5)], None, True) + invariant( + multi_locus.to_position, + Coord(0), + multi_locus.to_coordinate, + Point(position=1, offset=1, region='d'), + ) + invariant( + multi_locus.to_position, + Coord(1), + multi_locus.to_coordinate, + Point(position=1, offset=0, region=''), + ) + invariant( + multi_locus.to_position, + Coord(2), + multi_locus.to_coordinate, + Point(position=1, offset=-1, region=''), + ) + invariant( + multi_locus.to_position, + Coord(3), + multi_locus.to_coordinate, + Point(position=0, offset=1, region=''), + ) + invariant( + multi_locus.to_position, + Coord(4), + multi_locus.to_coordinate, + Point(position=0, offset=0, region=''), + ) + invariant( + multi_locus.to_position, + Coord(5), + multi_locus.to_coordinate, + Point(position=0, offset=-1, region='u'), ) \ No newline at end of file From c3c498486977d38153f9fab95ff32674d80199bd Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 18 Aug 2026 16:20:37 +0200 Subject: [PATCH 173/236] Rephrase error message. --- mutalyzer_crossmapper/multi_locus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index b3655c6..5f7409b 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -190,7 +190,7 @@ def to_coordinate(self, point: Point) -> int: except IndexError as e: if "Position" in str(e): raise IndexError( - f"Position {point.position} exceeds locus length {self._offsets[self._direction(index)] + self._loci[self._direction(index)].boundary[1] - self._loci[self._direction(index)].boundary[0]}" + f"Position {point.position} exceeds multi_locus length {self._end}" ) from e raise e From a188472b26900a8171da5328b6f2e66f3d11aa82 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 19 Aug 2026 09:49:25 +0200 Subject: [PATCH 174/236] Add Coord, multi_locus Point dataclass. --- docs/api/coord.rst | 5 +++++ docs/api/dataclass.rst | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 docs/api/coord.rst create mode 100644 docs/api/dataclass.rst diff --git a/docs/api/coord.rst b/docs/api/coord.rst new file mode 100644 index 0000000..6210702 --- /dev/null +++ b/docs/api/coord.rst @@ -0,0 +1,5 @@ +Coord +========= + +.. autoclass:: mutalyzer_crossmapper.locus.Coord + :members: \ No newline at end of file diff --git a/docs/api/dataclass.rst b/docs/api/dataclass.rst new file mode 100644 index 0000000..19988d9 --- /dev/null +++ b/docs/api/dataclass.rst @@ -0,0 +1,20 @@ +Dataclass +========= + +.. autoclass:: mutalyzer_crossmapper.locus.Point + :members: + +.. autoclass:: mutalyzer_crossmapper.multi_locus.Point + :members: + +.. autoclass:: mutalyzer_crossmapper.crossmapper.GenomicPoint + :inherited-members: + :members: + +.. autoclass:: mutalyzer_crossmapper.crossmapper.NonCodingPoint + :inherited-members: + :members: + +.. autoclass:: mutalyzer_crossmapper.crossmapper.CodingPoint + :inherited-members: + :members: From 30a2bb1a4b5a0e0f58802c2b5e64d2acab00f4ae Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 19 Aug 2026 09:57:23 +0200 Subject: [PATCH 175/236] Update documentation. --- docs/library.rst | 541 ++++++++++++----------------------------------- 1 file changed, 135 insertions(+), 406 deletions(-) diff --git a/docs/library.rst b/docs/library.rst index 39681ab..52901b3 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -1,7 +1,17 @@ Library ======= -The library provides a number of classes to perform various conversions. +The package provides conversion helpers between zero-based genomic +coordinates and HGVS-like point models for genomic, non-coding, coding, +and protein contexts. + +Coordinate And Location Conventions +----------------------------------- + +- Coordinates are zero-based integers. +- Locations are provided as half-open intervals: ``(start, end)`` with + ``start`` inclusive and ``end`` exclusive. +- HGVS-style positions exposed by public dataclasses are one-based. The ``Genomic`` class @@ -10,15 +20,16 @@ The ``Genomic`` class The ``Genomic`` class provides an interface to conversions between genomic (``g.``, ``m.``, ``o.``) positions and coordinates. -Genomic Position Model -~~~~~~~~~~~~~~~~~~~~~~~ +The ``GenomicPoint`` dataclass +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Genomic positions follow the HGVS genomic coordinate system. -They are represented as 1-key dictionaries. Below is an example of ``g.1`` in HGVS. +They are represented as 1-attribute dataclasses. Below is an example of ``g.1`` in HGVS. .. code-block:: python - {'position': 1} + >>> from mutalyzer_crossmapper import GenomicPoint + >>> GenomicPoint(position=1) Where: @@ -27,7 +38,7 @@ Where: Genomic Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code:: python +.. code-block:: python >>> from mutalyzer_crossmapper import Genomic >>> crossmap = Genomic() @@ -38,8 +49,8 @@ used to convert to and from genomic positions. .. code:: python >>> crossmap.coordinate_to_genomic(0) - {'position': 1} - >>> crossmap.genomic_to_coordinate({'position': 1}) + GenomicPoint(position=1) + >>> crossmap.genomic_to_coordinate(GenomicPoint(position=1)) 0 See section :doc:`api/crossmap` for a detailed description. @@ -52,493 +63,211 @@ On top of the functionality provided by the ``Genomic`` class, the (``n.``, ``r.``) positions and coordinates. Conversions between positioning systems should be done via a coordinate. -NonCoding Position Model -~~~~~~~~~~~~~~~~~~~~~~~~ - -Noncoding positions follow the HGVS ``n.`` coordinate system. They are represented -as 3-key dictionaries. Below is an example of ``n.14+1`` in HGVS. +The ``NonCodingPoint`` Dataclass +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: python +``NonCodingPoint`` fields: - { - 'position': 14, - 'offset': 1, - 'region': '' - } +- ``position``: positive integer (1-based transcript position) +- ``offset``: integer intronic/outside offset +- ``region``: one of ``''``, ``'u'``, ``'d'`` -Where: +.. code-block:: python -- **position**: an integer representing a nucleotide position (> 0) -- **offset**: an integer indicating the offset relative to the position (negative for upstream, - positive for downstream) -- **region**: a string describing the region type (empty for positions within a non-coding - transcript, ``u`` for upstream, ``d`` for downstream) + >>> from mutalyzer_crossmapper import NonCodingPoint + >>> NonCodingPoint(position=14, offset=1, region='') + NonCodingPoint(position=14, offset=1, region='') -NonCoding Position Conversion -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Non-Coding Position Conversion +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code:: python +.. code-block:: python >>> from mutalyzer_crossmapper import NonCoding >>> exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] >>> crossmap = NonCoding(exons) - -Now the functions ``coordinate_to_noncoding()`` and ``noncoding_to_coordinate()`` -can be used. - -In our example, the HGVS position ``g.36`` (coordinate *35*) is equivalent to -position ``n.14+1``. We can convert between these two as follows. - -.. code:: python - >>> crossmap.coordinate_to_noncoding(35) - {'position': 14, 'offset': 1, 'region': ''} - >>> crossmap.noncoding_to_coordinate({'position': 14, 'offset': 1, 'region': ''}) + NonCodingPoint(position=14, offset=1, region='') + >>> crossmap.noncoding_to_coordinate(NonCodingPoint(position=14, offset=1, region='')) 35 -When the coordinate is upstream or downstream of the transcript, we use ``u`` to -denote upstream and ``d`` to denote downstream. +Upstream and downstream positions are represented using ``region='u'`` and +``region='d'``: -.. code:: python +.. code-block:: python >>> crossmap.coordinate_to_noncoding(2) - {'position': 1, 'offset': -3, 'region': 'u'} - >>> crossmap.noncoding_to_coordinate({'position': 1, 'offset': -3, 'region': 'u'}) - 2 + NonCodingPoint(position=1, offset=-3, region='u') >>> crossmap.coordinate_to_noncoding(73) - {'position': 22, 'offset': 2, 'region': 'd'} - >>> crossmap.noncoding_to_coordinate({'position': 22, 'offset': 2, 'region': 'd'}) - 73 + NonCodingPoint(position=22, offset=2, region='d') -For transcripts that reside on the reverse complement strand, the ``inverted`` -parameter should be set to ``True``. In our example, HGVS position ``g.36`` -(coordinate *35*) is now equivalent to position ``n.9-1``. +For reverse-complement orientation, set ``inverted=True``: -.. code:: python +.. code-block:: python - >>> crossmap = NonCoding(exons, inverted=True) - >>> crossmap.coordinate_to_noncoding(35) - {'position': 9, 'offset': -1, 'region': ''} - >>> crossmap.noncoding_to_coordinate({'position': 9, 'offset': -1, 'region': ''}) - 35 + >>> reverse = NonCoding(exons, inverted=True) + >>> reverse.coordinate_to_noncoding(35) + NonCodingPoint(position=9, offset=-1, region='') -In the following table, we show a number of annotated examples. - -.. _table_noncoding: -.. list-table:: Coordinates to Noncoding Positions mapping. - :header-rows: 1 - - * - coordinate - - position - - offset - - region - - HGVS - * - 0 - - 1 - - -5 - - ``u`` - - ``n.u5`` - * - 4 - - 1 - - -1 - - ``u`` - - ``n.u1`` - * - 5 - - 1 - - 0 - - - - ``n.1`` - * - 24 - - 9 - - 5 - - - - ``n.9+5`` - * - 25 - - 10 - - -5 - - - - ``n.10-5`` - * - 71 - - 22 - - 0 - - - - ``n.22`` - * - 72 - - 22 - - 1 - - ``d`` - - ``n.d1`` - * - 79 - - 22 - - 8 - - ``d`` - - ``n.d8`` +See :doc:`api/crossmap` for full API details. -See section :doc:`api/crossmap` for a detailed description. -The ``Coding`` class +The ``Coding`` Class -------------------- -The ``Coding`` class provides an interface to all conversions between -coding (``c.``, ``r.``) positions and coordinates. Conversions between -positioning systems should be done via a coordinate. +``Coding`` extends ``NonCoding`` with coding DNA position logic +(``c.``, ``r.``), using exon locations and one CDS interval. -Coding Position Model -~~~~~~~~~~~~~~~~~~~~~ -Coding positions follow the HGVS ``c.`` coordinate system. They are -represented as 3-key dictionaries. Here is an example of ``c.*1+3``. +The ``CodingPoint`` Dataclass +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: python +``CodingPoint`` fields: - { - 'position': 1, - 'offset': 3, - 'region': '*' - } +- ``position``: positive integer +- ``offset``: integer +- ``region``: one of ``''``, ``'u'``, ``'d'``, ``'-'``, ``'*'`` -Where: +.. code-block:: python -- **position**: an integer representing a transcript position (> 0) -- **offset**: an integer indicating the offset relative to the position (negative for upstream, - positive for downstream) -- **region**: a string describing the region type (empty for positions within coding DNA sequence, - ``-`` for 5' UTR, ``*`` for 3' UTR, ``u`` for upstream and ``d`` for downstream) + >>> from mutalyzer_crossmapper import CodingPoint + >>> CodingPoint(position=1, offset=3, region='*') + CodingPoint(position=1, offset=3, region='*') Coding Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code:: python +.. code-block:: python >>> from mutalyzer_crossmapper import Coding >>> exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] >>> cds = (32, 43) >>> crossmap = Coding(exons, cds) - -On top of the functionality provided by the ``NonCoding`` class, the functions -``coordinate_to_coding()`` and ``coding_to_coordinate()`` can be used. These -functions use a 3-key dictionary to represent a coding position. - -In our example, the HGVS position ``g.32`` (coordinate *31*) is equivalent to -position ``c.-1``. We can convert between these two as follows. - -.. code:: python - >>> crossmap.coordinate_to_coding(31) - {'position': 1, 'offset': 0, 'region': '-'} - >>> crossmap.coding_to_coordinate({'position': 1, 'offset': 0, 'region': '-'}) + CodingPoint(position=1, offset=0, region='-') + >>> crossmap.coding_to_coordinate(CodingPoint(position=1, offset=0, region='-')) 31 -The ``coordinate_to_coding()`` function accepts an optional ``degenerate`` -argument. When set to ``True``, positions outside of the transcript are no -longer described using the ``u`` or ``d`` notation, ``-`` and ``*`` are used -instead. Note that the values of ``position`` and ``offset`` are adjusted accordingly. +The optional ``degenerate=True`` argument maps outside-transcript ``u``/``d`` +representations to degenerate ``-``/``*`` positions: -.. code:: python +.. code-block:: python >>> crossmap.coordinate_to_coding(4) - {'position': 11, 'offset': -1, 'region': 'u'} - >>> crossmap.coordinate_to_coding(4, True) - {'position': 12, 'offset': 0, 'region': '-'} - -In the following table, we show a number of annotated examples. - -.. _table_coding: -.. list-table:: Coordinates to Coding Positions mapping - :header-rows: 1 - - * - coordinate - - position - - offset - - region - - HGVS - * - 0 - - 11 - - -5 - - ``u`` - - ``c.u5`` - * - 4 - - 11 - - -1 - - ``u`` - - ``c.u1`` - * - 5 - - 11 - - 0 - - ``-`` - - ``c.-11`` - * - 24 - - 3 - - 5 - - ``-`` - - ``c.-3+5`` - * - 31 - - 1 - - 0 - - ``-`` - - ``c.-1`` - * - 32 - - 1 - - 0 - - - - ``c.1`` - * - 37 - - 3 - - 3 - - - - ``c.3+3`` - * - 38 - - 4 - - -2 - - - - ``c.4-2`` - * - 43 - - 1 - - 0 - - ``*`` - - ``c.*1`` - * - 61 - - 4 - - -9 - - ``*`` - - ``c.*4-9`` - * - 71 - - 5 - - 0 - - ``*`` - - ``c.*5`` - * - 72 - - 5 - - 1 - - ``d`` - - ``c.d1`` - * - 79 - - 5 - - 8 - - ``d`` - - ``c.d8`` - - -Protein -------- - -Additionally, the functions ``coordinate_to_protein()`` and -``protein_to_coordinate()`` can be used. These functions use a 4-key dictionary -to represent a protein position. Here is one example of three possibilities -for ``p.1`` in HGVS. + CodingPoint(position=11, offset=-1, region='u') + >>> crossmap.coordinate_to_coding(4, degenerate=True) + CodingPoint(position=12, offset=0, region='-') -.. code-block:: python - { - 'position': 1, - 'position_in_codon': 3, - 'offset': 0, - 'region': '' - } +Protein Conversion +------------------ -Where: +``Coding`` also exposes conversion to and from protein-position models. -- **position**: an integer representing an amino acid position (> 0) -- **position_in_codon**: an integer indexing the position in a codon (1, 2, or 3) -- **offset**: an integer indicating offset relative to the nucleotide specified by `position_in_codon` in the codon -- **region**: a string describing the region type (empty for valid amino acid positions) +The ``ProteinPoint`` Dataclass +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In our example, the HGVS position ``g.42`` (coordinate *41*) corresponds with -position ``p.2``. We can convert between these two as follows. +``ProteinPoint`` extends ``CodingPoint`` with: -.. code:: python +- ``position_in_codon``: one of ``1``, ``2``, ``3`` + +.. code-block:: python + + >>> from mutalyzer_crossmapper import ProteinPoint + >>> ProteinPoint(position=1, position_in_codon=3, offset=0, region='') + ProteinPoint(position=1, offset=0, region='', position_in_codon=3) +.. code-block:: python + + >>> from mutalyzer_crossmapper import Coding + >>> exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] + >>> cds = (32, 43) + >>> crossmap = Coding(exons, cds) >>> crossmap.coordinate_to_protein(41) - {'position': 2, 'position_in_codon': 2, 'offset': 0, 'region': ''} - >>> crossmap.protein_to_coordinate({'position': 2, 'position_in_codon': 2, 'offset': 0, 'region': ''}) + ProteinPoint(position=2, offset=0, region='', position_in_codon=2) + >>> crossmap.protein_to_coordinate( + ... ProteinPoint(position=2, offset=0, region='', position_in_codon=2) + ... ) 41 -**Note:** protein position only corresponds with the HGVS "p." notation -when the offset equals ``0`` and the region equals empty. In the following -table, we show a number of annotated examples. - -.. _table_protein: -.. list-table:: Coordinates to Protein Positions mapping - :header-rows: 1 - - * - coordinate - - position - - position_in_codon - - offset - - region - - HGVS - * - 0 - - 4 - - 2 - - -5 - - ``u`` - - - * - 4 - - 4 - - 2 - - -1 - - ``u`` - - - * - 5 - - 4 - - 2 - - 0 - - ``-`` - - - * - 31 - - 1 - - 3 - - 0 - - ``-`` - - - * - 32 - - 1 - - 1 - - 0 - - - - ``p.1`` - * - 33 - - 1 - - 2 - - 0 - - - - ``p.1`` - * - 34 - - 1 - - 3 - - 0 - - - - ``p.1`` - * - 35 - - 1 - - 3 - - 1 - - - - - * - 42 - - 2 - - 3 - - 0 - - - - ``p.2`` - * - 43 - - 1 - - 1 - - 0 - - ``*`` - - - * - 44 - - 1 - - 1 - - 1 - - ``*`` - - - * - 72 - - 2 - - 2 - - 1 - - ``d`` - - - - * - 79 - - 2 - - 2 - - 8 - - ``d`` - - +Note that canonical HGVS ``p.`` positions correspond to points with +``offset == 0`` and ``region == ''``. +See :doc:`api/crossmap` for full API details. -See section :doc:`api/crossmap` for a detailed description. -Locations ---------- +Location Helper +--------------- -In many cases we need to know the nearest location with respect to a -coordinate. For example, we need to know where the nearest exon is when we want -to describe a position in an intron. The ``nearest_location()`` can be used to -do exactly this. +``nearest_location()`` finds the closest location index for a coordinate. +When two boundaries are equally close, ``p`` controls tie-breaking +(``0``: left, ``1``: right). -.. code:: python +.. code-block:: python >>> from mutalyzer_crossmapper import nearest_location + >>> exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] >>> nearest_location(exons, 37) 2 - >>> nearest_location(exons, 38) + >>> nearest_location(exons, 37, p=1) 3 -Notice that coordinate ``37`` is in the center of intron 2. By default -``nearest_location()`` will return the left location in case of a draw. This -behaviour can be altered by setting the optional argument ``p`` to ``1``. +See :doc:`api/location` for full API details. -.. code:: python - - >>> nearest_location(exons, 37, 1) - 3 -See section :doc:`api/location` for a detailed description. - -Basic classes +Basic Classes ------------- -The ``Coding`` class makes use of a number of basic classes described in this -section. +These lower-level classes are used by ``NonCoding`` and ``Coding``. -The ``Locus`` class -~~~~~~~~~~~~~~~~~~~ +The ``Point`` Dataclass +~~~~~~~~~~~~~~~~~~~~~~~ -The ``Locus`` class is used to deal with offsets with respect to a single -locus. +``Point`` is the internal coordinate model used by ``Locus`` and +``MultiLocus``. -.. code:: python +- ``position``: zero-based position within a locus or concatenated loci +- ``offset``: relative offset +- ``region``: one of ``''``, ``'u'``, ``'d'`` (mainly for ``MultiLocus``) - >>> from mutalyzer_crossmapper import Locus - >>> locus = Locus((10, 20)) +See :doc:`api/dataclass`. -This class provides the functions ``to_position()`` and ``to_coordinate()`` for -converting from a locus position to a coordinate and vice versa. These -functions work with a 2-key dictionary, see the section about `The NonCoding class`_ -for the semantics. -**Note:** the ``position`` values in the position dictionaries are **0-based**, -so the first base of the locus corresponds to ``{'position': 0, 'offset': 0}``. -This differs from HGVS numbering, which is **1-based**. +The ``Locus`` Class +~~~~~~~~~~~~~~~~~~~ -.. code:: python +``Locus`` maps one genomic interval to/from ``Point``. +.. code-block:: python + + >>> from mutalyzer_crossmapper.locus import Locus, Point, Coord + >>> locus = Locus((10, 20)) >>> locus.to_position(9) - {'position': 0, 'offset': -1} - >>> locus.to_coordinate({'position': 0, 'offset': -1}) + Point(position=0, offset=-1) + >>> locus.to_coordinate(Point(position=0, offset=-1)) 9 -For loci that reside on the reverse complement strand, the optional -``inverted`` constructor parameter should be set to ``True``. +Set ``inverted=True`` for reverse-complement orientation. -See section :doc:`api/locus` for a detailed description. +See :doc:`api/locus` for full API details. -The ``MultiLocus`` class -^^^^^^^^^^^^^^^^^^^^^^^^ -The ``MultiLocus`` class is used to deal with offsets with respect to multiple -loci. - -.. code:: python - - >>> from mutalyzer_crossmapper import MultiLocus - >>> multilocus = MultiLocus([(10, 20), (40, 50)]) - -The interface to this class is similar to that of the ``Locus`` class. Functions -``to_position()`` and ``to_coordinate()`` work with a 3-key dictionary. +The ``MultiLocus`` Class +~~~~~~~~~~~~~~~~~~~~~~~~ -**Note:** again, the ``position`` values in the position dictionaries are **0-based**. +``MultiLocus`` maps coordinates across multiple intervals to/from a unified +``Point`` model. -.. code:: python +.. code-block:: python + >>> from mutalyzer_crossmapper import MultiLocus, Point + >>> multilocus = MultiLocus([(10, 20), (40, 50)]) >>> multilocus.to_position(22) - {'position': 9, 'offset': 3, 'region': ''} - >>> multilocus.to_coordinate({'position': 9, 'offset': 3, 'region': ''}) + Point(position=9, offset=3, region='') + >>> multilocus.to_coordinate(Point(position=9, offset=3, region='')) 22 >>> multilocus.to_position(38) - {'position': 10, 'offset': -2, 'region': ''} - >>> multilocus.to_coordinate({'position': 10, 'offset': -2, 'region': ''}) + Point(position=10, offset=-2, region='') + >>> multilocus.to_coordinate(Point(position=10, offset=-2, region='')) 38 -See section :doc:`api/multi_locus` for a detailed description. +See :doc:`api/multi_locus` for full API details. From e9a99a5732bfa3341291ee8a2b45b165f66f1b7e Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 19 Aug 2026 09:59:30 +0200 Subject: [PATCH 176/236] Move Corrd into locus. --- docs/api.rst | 1 + docs/api/coord.rst | 5 ----- docs/api/locus.rst | 3 +++ 3 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 docs/api/coord.rst diff --git a/docs/api.rst b/docs/api.rst index 1d18302..1f7c54e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -10,3 +10,4 @@ API documentation api/location api/locus api/multi_locus + api/dataclass diff --git a/docs/api/coord.rst b/docs/api/coord.rst deleted file mode 100644 index 6210702..0000000 --- a/docs/api/coord.rst +++ /dev/null @@ -1,5 +0,0 @@ -Coord -========= - -.. autoclass:: mutalyzer_crossmapper.locus.Coord - :members: \ No newline at end of file diff --git a/docs/api/locus.rst b/docs/api/locus.rst index 93aadff..0a98eaa 100644 --- a/docs/api/locus.rst +++ b/docs/api/locus.rst @@ -3,3 +3,6 @@ Locus .. automodule:: mutalyzer_crossmapper.locus :members: + +.. autoclass:: mutalyzer_crossmapper.locus.Coord + :members: \ No newline at end of file From e748ef52b9604b432bf084401dbdc7c6956cea1c Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 19 Aug 2026 11:51:59 +0200 Subject: [PATCH 177/236] WIP: Add checks in crossmaper and tests. --- mutalyzer_crossmapper/crossmapper.py | 131 ++++++---- mutalyzer_crossmapper/multi_locus.py | 1 + tests/helper.py | 3 +- tests/test_crossmapper.py | 370 ++++++++++++--------------- 4 files changed, 247 insertions(+), 258 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 0aeeae4..d0f55ea 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -1,7 +1,8 @@ from dataclasses import dataclass -from .multi_locus import MultiLocus -from .locus import Point +from .multi_locus import MultiLocus, Point, Coord, _check_in_range, _check_multi_locus +from .locus import _check_locus, _check_non_negative_int, _check_int +from .location import nearest_location @dataclass(slots=True) class GenomicPoint: @@ -9,8 +10,7 @@ class GenomicPoint: position: int def __post_init__(self) -> None: - if not isinstance(self.position, int) or self.position <= 0: - raise ValueError('Position must be a positive integer') + _check_non_negative_int(self.position) def __str__(self) -> str: return f'{self.position}' @@ -18,24 +18,24 @@ def __str__(self) -> str: class Genomic(object): """Genomic crossmap object.""" - - def coordinate_to_genomic(self, coordinate: int) -> GenomicPoint: + def coordinate_to_genomic(self, coord: Coord, length: int | None = None) -> GenomicPoint: """Convert a coordinate to a genomic point model (g./m./o.). - :arg int coordinate: Coordinate. + :arg Coord coordinate: Coordinate model :returns GenomicPoint: Genomic point model. """ - return GenomicPoint(coordinate + 1) + _check_in_range(coord.coordinate, length) + return GenomicPoint(coord.coordinate + 1) - def genomic_to_coordinate(self, point: GenomicPoint) -> int: + def genomic_to_coordinate(self, point: GenomicPoint) -> Coord: """Convert a genomic point (g./m./o.) to a coordinate. :arg GenomicPoint point: Genomic point model. - :returns int: Coordinate. + :returns Coord: Coordinate model. """ - return point.position - 1 + return Coord(point.position - 1) @dataclass(slots=True) @@ -50,49 +50,50 @@ def __post_init__(self) -> None: # Python version 3.11 and 3.10: cannot use super() due to conflicts with slots=True GenomicPoint.__post_init__(self) - if not isinstance(self.offset, int): - raise TypeError('Offset must be an integer') + _check_int(self.offset) if self.region not in self.allowed_regions: raise ValueError(f'Region must be a string in {self.allowed_regions}') def __str__(self) -> str: if self.offset == 0: return f'{self.region}{self.position}' + if self.region in ('u', 'd'): + return f'{self.region}{abs(self.offset)}' return f'{self.region}{self.position}{self.offset:+}' class NonCoding(Genomic): """NonCoding crossmap object.""" - def __init__(self, locations: list[tuple[int, int]], inverted: bool = False) -> None: + def __init__(self, locations: list[tuple[int, int]], length: int | None = None, inverted: bool = False) -> None: """ :arg list locations: List of locus locations. :arg bool inverted: Orientation. """ + _check_multi_locus(locations, length) self._inverted = inverted + self._noncoding = MultiLocus(locations, length, inverted) - self._noncoding = MultiLocus(locations, inverted) - - def coordinate_to_noncoding(self, coordinate: int) -> NonCodingPoint: + def coordinate_to_noncoding(self, coord: Coord) -> NonCodingPoint: """Convert a coordinate to a noncoding point model (n./r.). - :arg int coordinate: Coordinate. + :arg Coord coord: Coordinate model. :returns NonCodingPoint: Noncoding point model. """ - point = self._noncoding.to_position(coordinate) + point = self._noncoding.to_position(coord) return NonCodingPoint( position=point.position + 1, offset=point.offset, region=point.region ) - def noncoding_to_coordinate(self, point: NonCodingPoint) -> int: - """Convert a noncoding point (n./r.) to a coordinate. + def noncoding_to_coordinate(self, point: NonCodingPoint) -> Coord: + """Convert a noncoding point (n./r.) to a coordinate model. :arg NonCodingPoint point: Noncoding point model. - :returns int: Coordinate. + :returns Coord: Coordinate model. """ return self._noncoding.to_coordinate( Point( @@ -132,6 +133,7 @@ def __init__( self, locations: list[tuple[int, int]], cds: tuple[int, int], + length: int|None = None, inverted: bool = False ) -> None: """ @@ -139,12 +141,13 @@ def __init__( :arg tuple cds: Locus location. :arg bool inverted: Orientation. """ - NonCoding.__init__(self, locations, inverted) + NonCoding.__init__(self, locations, length, inverted) + self._check_cds(cds, locations, length) - cds_start = self._noncoding.to_position(cds[0]) - cds_end = self._noncoding.to_position(cds[1] - 1) - exon_start = self._noncoding.to_position(locations[0][0]) - exon_end = self._noncoding.to_position(locations[-1][1] - 1) + cds_start = self._noncoding.to_position(Coord(cds[0])) + cds_end = self._noncoding.to_position(Coord(cds[1] - 1)) + exon_start = self._noncoding.to_position(Coord(locations[0][0])) + exon_end = self._noncoding.to_position(Coord(locations[-1][1] - 1)) if self._inverted: self._coding = ( @@ -164,16 +167,25 @@ def __init__( exon_start.position + exon_start.offset, exon_end.position + exon_end.offset + 1 ) - - def _coordinate_to_coding(self, coordinate: int) -> CodingPoint: + print(f'Coding: {self._coding}, Exons: {self._exons}') + + def _check_cds(self, cds: tuple[int, int], locations: list[tuple[int, int]], length: int|None = None) -> None: + """Check if the CDS is valid.""" + _check_locus(cds) + _check_in_range(cds[1], length) + for coord in cds: + index = nearest_location(locations, coord) + if coord < locations[index][0] or coord > locations[index][1]: + raise ValueError(f"Coordinate {coord} of CDS {cds} is not within any exon.") + + def _coordinate_to_coding(self, coord: Coord) -> CodingPoint: """Convert a coordinate to a coding point model (c./r.). - #TODO: explain why checking - :arg int coordinate: Coordinate. + :arg Coord coord: Coordinate model. :returns CodingPoint: Coding position model (c./r.). """ - noncoding_point = self._noncoding.to_position(coordinate) + noncoding_point = self._noncoding.to_position(coord) position = noncoding_point.position offset = noncoding_point.offset @@ -200,16 +212,15 @@ def _coordinate_to_coding(self, coordinate: int) -> CodingPoint: region = '' return CodingPoint(position=position, offset=offset, region=region) - def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> CodingPoint: + def coordinate_to_coding(self, coord: Coord, degenerate: bool = False) -> CodingPoint: """Convert a coordinate to a coding point model (c./r.). - # TODO: explain abs() - :arg int coordinate: Coordinate. + :arg Coord coord: Coordinate model. :arg bool degenerate: Return a degenerate position. :returns CodingPoint: Coding point model (c./r.). """ - point = self._coordinate_to_coding(coordinate) + point = self._coordinate_to_coding(coord) if not degenerate: return point @@ -229,7 +240,7 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> Cod return CodingPoint(position=position, offset=0, region='*') return point - def coding_to_coordinate(self, point: CodingPoint) -> int: + def _coding_to_coordinate(self, point: CodingPoint) -> int: """Convert a coding position (c./r.) to a coordinate. :arg CodingPoint point: Coding point model (c./r.). @@ -237,13 +248,24 @@ def coding_to_coordinate(self, point: CodingPoint) -> int: :returns int: Coordinate. """ region = point.region + position = point.position if region in ('u', 'd'): + if region == 'u': + if self._coding[0] == self._exons[0]: + position = 1 + else: + position = 1 + if region == 'd': + if self._coding[1] == self._exons[1]: + position = self._coding[1] + else: + position = position + self._coding[1] return self._noncoding.to_coordinate( - Point(position=point.position, region=point.region, offset=point.offset) + Point(position=position - 1, region=point.region, offset=point.offset) ) - position = point.position + if region == '': position = position + self._coding[0] - 1 elif region == '-': @@ -254,14 +276,37 @@ def coding_to_coordinate(self, point: CodingPoint) -> int: Point(position=position, region='', offset=point.offset) ) - def coordinate_to_protein(self, coordinate: int) -> ProteinPoint: - """Convert a coordinate to a protein point model (p.). + def coding_to_coordinate(self, point: CodingPoint) -> int: + """Convert a coding position (c./r.) to a coordinate. + + :arg CodingPoint point: Coding point model (c./r.). + + :returns int: Coordinate. + """ + # Check if the point is degenerate + # return self._coding_to_coordinate(point) + region = point.region + offset = point.offset + position = point.position - :arg int coordinate: Coordinate. + if region == '-' and offset == 0: + if position > self._coding[0]: + point = CodingPoint(position=self._coding[0], offset=self._coding[0] - position, region='u') + if region == '*' and offset == 0: + print("innnnnn") + if position > self._exons[1] -self._coding[1]: + point = CodingPoint(position=self._exons[1] -self._coding[1], offset=position - (self._exons[1] -self._coding[1]), region='d') + print(f'Coding to coordinate: position={point.position}, offset={point.offset}, region={point.region}') + return self._coding_to_coordinate(point) + + def coordinate_to_protein(self, coord: Coord) -> ProteinPoint: + """Convert a coordinate to a protein point model (p.). +point.position + :arg Coord coord: Coordinate model. :returns ProteinPoint: Protein point model(p.). """ - point = self.coordinate_to_coding(coordinate) + point = self.coordinate_to_coding(coord) position = point.position if point.region in ('-', 'u'): diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 5f7409b..d97fa71 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -80,6 +80,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - :arg int offset: Offset. :arg str region: Region. """ + print("offsets", self._offsets) # Upstream region validation, position is constant value and offset should not be positive if region == 'u': if position != self._offsets[0]: diff --git a/tests/helper.py b/tests/helper.py index a17b119..c3205b1 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -5,5 +5,4 @@ def invariant(f, x, f_i, y): def degenerate_equal(f, coordinate, locations): assert f(locations[0]) == coordinate - assert len( - set(map(f, locations))) == 1 + assert len(set(obj.coordinate for obj in map(f, locations))) == 1 diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 2ecd887..d6033fa 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -1,6 +1,7 @@ -from mutalyzer_crossmapper import Genomic, NonCoding, Coding, GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint +from mutalyzer_crossmapper.crossmapper import Coord, Genomic, NonCoding, Coding, GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint from helper import degenerate_equal, invariant +import pytest _exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] _cds = (32, 43) @@ -12,13 +13,13 @@ def test_Genomic(): invariant( crossmap.coordinate_to_genomic, - 0, + Coord(0), crossmap.genomic_to_coordinate, GenomicPoint(position=1), ) invariant( crossmap.coordinate_to_genomic, - 98, + Coord(98), crossmap.genomic_to_coordinate, GenomicPoint(position=99), ) @@ -31,19 +32,19 @@ def test_NonCoding(): # Boundary between upstream and transcript. invariant( crossmap.coordinate_to_noncoding, - 3, + Coord(3) , crossmap.noncoding_to_coordinate, NonCodingPoint(position=1, offset=-2, region='u'), ) invariant( crossmap.coordinate_to_noncoding, - 4, + Coord(4), crossmap.noncoding_to_coordinate, NonCodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_noncoding, - 5, + Coord(5), crossmap.noncoding_to_coordinate, NonCodingPoint(position=1, offset=0, region=''), ) @@ -51,13 +52,13 @@ def test_NonCoding(): # Boundary between downstream and transcript. invariant( crossmap.coordinate_to_noncoding, - 71, + Coord(71), crossmap.noncoding_to_coordinate, NonCodingPoint(position=22, offset=0, region=''), ) invariant( crossmap.coordinate_to_noncoding, - 72, + Coord(72), crossmap.noncoding_to_coordinate, NonCodingPoint(position=22, offset=1, region='d'), ) @@ -70,13 +71,13 @@ def test_NonCoding_inverted(): # Boundary between upstream and transcript. invariant( crossmap.coordinate_to_noncoding, - 72, + Coord(72), crossmap.noncoding_to_coordinate, NonCodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_noncoding, - 71, + Coord(71), crossmap.noncoding_to_coordinate, NonCodingPoint(position=1, offset=0, region=''), ) @@ -84,70 +85,18 @@ def test_NonCoding_inverted(): # Boundary between downstream and transcript. invariant( crossmap.coordinate_to_noncoding, - 5, + Coord(5), crossmap.noncoding_to_coordinate, NonCodingPoint(position=22, offset=0, region=''), ) invariant( crossmap.coordinate_to_noncoding, - 4, + Coord(4), crossmap.noncoding_to_coordinate, NonCodingPoint(position=22, offset=1, region='d'), ) -def test_NonCoding_degenerate(): - """Forward oriented noncoding transcript.""" - crossmap = NonCoding(_exons) - - # Boundary between upstream and transcript. - degenerate_equal( - crossmap.noncoding_to_coordinate, - 4, - [ - NonCodingPoint(position=1, offset=-1, region=''), - NonCodingPoint(position=1, offset=-1, region='u'), - ], - ) - - # Boundary between downstream and transcript. - degenerate_equal( - crossmap.noncoding_to_coordinate, - 72, - [ - NonCodingPoint(position=22, offset=1, region='d'), - NonCodingPoint(position=22, offset=1, region=''), - NonCodingPoint(position=23, offset=0, region=''), - NonCodingPoint(position=24, offset=-1, region=''), - ], - ) - - -def test_NonCoding_inverted_degenerate(): - """Forward oriented noncoding transcript.""" - crossmap = NonCoding(_exons, inverted=True) - - # Boundary between upstream and transcript. - degenerate_equal( - crossmap.noncoding_to_coordinate, - 72, - [ - NonCodingPoint(position=1, offset=-1, region=''), - NonCodingPoint(position=1, offset=-1, region='u'), - ], - ) - - # Boundary between downstream and transcript. - degenerate_equal( - crossmap.noncoding_to_coordinate, - 4, - [ - NonCodingPoint(position=22, offset=1, region='d'), - NonCodingPoint(position=23, offset=0, region=''), - NonCodingPoint(position=22, offset=1, region=''), - ], - ) - def test_Coding(): """Forward oriented coding transcript.""" @@ -156,13 +105,13 @@ def test_Coding(): # Boundary between 5' and CDS. invariant( crossmap.coordinate_to_coding, - 31, + Coord(31), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='-'), ) invariant( crossmap.coordinate_to_coding, - 32, + Coord(32), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region=''), ) @@ -170,13 +119,13 @@ def test_Coding(): # Boundary between CDS and 3'. invariant( crossmap.coordinate_to_coding, - 42, + Coord(42), crossmap.coding_to_coordinate, CodingPoint(position=6, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, - 43, + Coord(43), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='*'), ) @@ -184,18 +133,18 @@ def test_Coding(): def test_Coding_inverted(): """Reverse oriented coding transcript.""" - crossmap = Coding(_exons, _cds, True) + crossmap = Coding(_exons, _cds, inverted=True) # Boundary between 5' and CDS. invariant( crossmap.coordinate_to_coding, - 43, + Coord(43), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='-'), ) invariant( crossmap.coordinate_to_coding, - 42, + Coord(42), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region=''), ) @@ -203,13 +152,13 @@ def test_Coding_inverted(): # Boundary between CDS and 3'. invariant( crossmap.coordinate_to_coding, - 32, + Coord(32), crossmap.coding_to_coordinate, CodingPoint(position=6, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, - 31, + Coord(31), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='*'), ) @@ -222,13 +171,13 @@ def test_Coding_regions(): # Upstream odd length intron between two regions. invariant( crossmap.coordinate_to_coding, - 25, + Coord(25), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=5, region='-'), ) invariant( crossmap.coordinate_to_coding, - 26, + Coord(26), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=-4, region=''), ) @@ -236,13 +185,13 @@ def test_Coding_regions(): # Downstream odd length intron between two regions. invariant( crossmap.coordinate_to_coding, - 44, + Coord(44), crossmap.coding_to_coordinate, CodingPoint(position=10, offset=5, region=''), ) invariant( crossmap.coordinate_to_coding, - 45, + Coord(45), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=-4, region='*'), ) @@ -250,18 +199,18 @@ def test_Coding_regions(): def test_Coding_regions_inverted(): """The CDS can start or end on a region boundary.""" - crossmap = Coding([(10, 21), (30, 40), (49, 60)], (30, 40), True) + crossmap = Coding([(10, 21), (30, 40), (49, 60)], (30, 40), inverted=True) # Upstream odd length intron between two regions. invariant( crossmap.coordinate_to_coding, - 44, + Coord(44), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=5, region='-'), ) invariant( crossmap.coordinate_to_coding, - 43, + Coord(43), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=-4, region=''), ) @@ -269,13 +218,13 @@ def test_Coding_regions_inverted(): # Downstream odd length intron between two regions. invariant( crossmap.coordinate_to_coding, - 25, + Coord(25), crossmap.coding_to_coordinate, CodingPoint(position=10, offset=5, region=''), ) invariant( crossmap.coordinate_to_coding, - 24, + Coord(24), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=-4, region='*'), ) @@ -288,13 +237,13 @@ def test_Coding_no_utr5(): # Direct transition from upstream to CDS. invariant( crossmap.coordinate_to_coding, - 9, + Coord(9), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_coding, - 10, + Coord(10), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region=''), ) @@ -305,18 +254,18 @@ def test_Coding_no_intron(): invariant( crossmap.coordinate_to_coding, - 20, + Coord(20), crossmap.coding_to_coordinate, CodingPoint(position=6, offset=0, region=''), ) def test_Coding_no_intron_inverted(): - crossmap = Coding([(10, 20), (20, 30)], (15, 25), True) + crossmap = Coding([(10, 20), (20, 30)], (15, 25), inverted=True) invariant( crossmap.coordinate_to_coding, - 20, + Coord(20), crossmap.coding_to_coordinate, CodingPoint(position=5, offset=0, region=''), ) @@ -327,18 +276,18 @@ def test_Coding_one_base_intron(): invariant( crossmap.coordinate_to_coding, - 19, + Coord(19), crossmap.coding_to_coordinate, CodingPoint(position=4, offset=1, region=''), ) def test_Coding_one_base_intron_inverted(): - crossmap = Coding([(10, 19), (20, 30)], (15, 25), True) + crossmap = Coding([(10, 19), (20, 30)], (15, 25), inverted=True) invariant( crossmap.coordinate_to_coding, - 19, + Coord(19), crossmap.coding_to_coordinate, CodingPoint(position=5, offset=1, region=''), ) @@ -346,18 +295,18 @@ def test_Coding_one_base_intron_inverted(): def test_Coding_no_utr5_inverted(): """A 5' UTR may be missing.""" - crossmap = Coding([(10, 20)], (15, 20), True) + crossmap = Coding([(10, 20)], (15, 20), inverted=True) # Direct transition from upstream to CDS. invariant( crossmap.coordinate_to_coding, - 20, + Coord(20), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_coding, - 19, + Coord(19), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region=''), ) @@ -366,17 +315,18 @@ def test_Coding_no_utr5_inverted(): def test_Coding_no_utr3(): """A 3' UTR may be missing.""" crossmap = Coding([(10, 20)], (15, 20)) - + print(crossmap.coordinate_to_coding(Coord(20)).position, crossmap.coordinate_to_coding(Coord(20)).offset, crossmap.coordinate_to_coding(Coord(20)).region) + print(crossmap.coding_to_coordinate(CodingPoint(position=5, offset=1, region='d'))) # Direct transition from CDS to downstream. invariant( crossmap.coordinate_to_coding, - 19, + Coord(19), crossmap.coding_to_coordinate, CodingPoint(position=5, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, - 20, + Coord(20), crossmap.coding_to_coordinate, CodingPoint(position=5, offset=1, region='d'), ) @@ -384,23 +334,22 @@ def test_Coding_no_utr3(): def test_Coding_no_utr3_inverted(): """A 3' UTR may be missing.""" - crossmap = Coding([(10, 20)], (10, 15), True) + crossmap = Coding([(10, 20)], (10, 15), inverted=True) # Direct transition from CDS to downstream. invariant( crossmap.coordinate_to_coding, - 10, + Coord(10), crossmap.coding_to_coordinate, CodingPoint(position=5, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, - 9, + Coord(9), crossmap.coding_to_coordinate, CodingPoint(position=5, offset=1, region='d'), ) - def test_Coding_small_utr5(): """A 5' UTR may be of length one.""" crossmap = Coding([(10, 20)], (11, 15)) @@ -408,19 +357,19 @@ def test_Coding_small_utr5(): # Transition from upstream to 5' UTR to CDS. invariant( crossmap.coordinate_to_coding, - 9, + Coord(9), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_coding, - 10, + Coord(10), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='-'), ) invariant( crossmap.coordinate_to_coding, - 11, + Coord(11), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region=''), ) @@ -428,24 +377,24 @@ def test_Coding_small_utr5(): def test_Coding_small_utr5_inverted(): """A 5' UTR may be of length one.""" - crossmap = Coding([(10, 20)], (15, 19), True) + crossmap = Coding([(10, 20)], (15, 19), inverted=True) # Transition from upstream to 5' UTR to CDS. invariant( crossmap.coordinate_to_coding, - 20, + Coord(20), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_coding, - 19, + Coord(19), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='-'), ) invariant( crossmap.coordinate_to_coding, - 18, + Coord(18), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region=''), ) @@ -458,19 +407,19 @@ def test_Coding_small_utr3(): # Transition from CDS to 3' UTR to downstream. invariant( crossmap.coordinate_to_coding, - 18, + Coord(18), crossmap.coding_to_coordinate, CodingPoint(position=4, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, - 19, + Coord(19), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='*'), ) invariant( crossmap.coordinate_to_coding, - 20, + Coord(20), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=1, region='d'), ) @@ -478,24 +427,24 @@ def test_Coding_small_utr3(): def test_Coding_small_utr3_inverted(): """A 5' UTR may be of length one.""" - crossmap = Coding([(10, 20)], (11, 15), True) + crossmap = Coding([(10, 20)], (11, 15), inverted=True) # Transition from CDS to 3' UTR to downstream. invariant( crossmap.coordinate_to_coding, - 11, + Coord(11), crossmap.coding_to_coordinate, CodingPoint(position=4, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, - 10, + Coord(10), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='*'), ) invariant( crossmap.coordinate_to_coding, - 9, + Coord(9), crossmap.coding_to_coordinate, CodingPoint(position=1, offset=1, region='d'), ) @@ -508,38 +457,29 @@ def test_Coding_degenerate(): # Degenerate position in upstream. degenerate_equal( crossmap.coding_to_coordinate, - 9, + Coord(9), [ CodingPoint(position=1, offset=-1, region='u'), CodingPoint(position=2, offset=0, region='-'), - CodingPoint(position=1, offset=-2, region=''), - CodingPoint(position=1, offset=-10, region='*'), - CodingPoint(position=2, offset=-11, region='*'), - CodingPoint(position=3, offset=1, region='-'), - CodingPoint(position=4, offset=2, region='-'), ], ) degenerate_equal( crossmap.coding_to_coordinate, - 20, + Coord(20), [ - CodingPoint(position=9, offset=1, region='d'), + CodingPoint(position=1, offset=1, region='d'), CodingPoint(position=2, offset=0, region='*'), - CodingPoint(position=8, offset=2, region=''), - CodingPoint(position=1, offset=10, region='-'), - CodingPoint(position=2, offset=11, region='-'), - CodingPoint(position=7, offset=3, region=''), ], ) def test_Coding_inverted_degenerate(): """Degenerate upstream and downstream positions are silently corrected.""" - crossmap = Coding([(10, 20)], (11, 19), True) + crossmap = Coding([(10, 20)], (11, 19), inverted=True) degenerate_equal( crossmap.coding_to_coordinate, - 20, + Coord(20), [ CodingPoint(position=1, offset=-1, region='u'), CodingPoint(position=2, offset=0, region='-'), @@ -551,7 +491,7 @@ def test_Coding_inverted_degenerate(): ) degenerate_equal( crossmap.coding_to_coordinate, - 9, + Coord(9), [ CodingPoint(position=2, offset=1, region='d'), CodingPoint(position=2, offset=0, region='*'), @@ -566,117 +506,118 @@ def test_Coding_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" crossmap = Coding([(10, 20)], (11, 19)) - assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=2, offset=0, region='-') - assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=2, offset=0, region='*') + assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=2, offset=0, region='-') + assert crossmap.coordinate_to_coding(Coord(20), True) == CodingPoint(position=2, offset=0, region='*') def test_Coding_inverted_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" - crossmap = Coding([(10, 20)], (11, 19), True) + crossmap = Coding([(10, 20)], (11, 19), inverted=True) - assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=2, offset=0, region='-') - assert crossmap.coordinate_to_coding(25, True) == CodingPoint(position=7, offset=0, region='-') - assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=2, offset=0, region='*') + assert crossmap.coordinate_to_coding(Coord(20), True) == CodingPoint(position=2, offset=0, region='-') + assert crossmap.coordinate_to_coding(Coord(25), True) == CodingPoint(position=7, offset=0, region='-') + assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=2, offset=0, region='*') def test_Coding_no_utr5_degenerate_return(): """A 5' UTR may be missing.""" crossmap = Coding([(10, 20)], (10, 15)) - assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=1, offset=0, region='-') - assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=1, offset=0, region='') - assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=5, offset=0, region='*') - assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=6, offset=0, region='*') + assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=1, offset=0, region='-') + assert crossmap.coordinate_to_coding(Coord(10), True) == CodingPoint(position=1, offset=0, region='') + assert crossmap.coordinate_to_coding(Coord(19), True) == CodingPoint(position=5, offset=0, region='*') + assert crossmap.coordinate_to_coding(Coord(20), True) == CodingPoint(position=6, offset=0, region='*') def test_Coding_no_utr5_inverted_degenerate_return(): """A 5' UTR may be missing.""" - crossmap = Coding([(10, 20)], (10, 15), True) + crossmap = Coding([(10, 20)], (15, 20), inverted=True) - assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=1, offset=0, region='*') - assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=5, offset=0, region='') - assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=5, offset=0, region='-') - assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=6, offset=0, region='-') + assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=1, offset=0, region='*') + assert crossmap.coordinate_to_coding(Coord(10), True) == CodingPoint(position=5, offset=0, region='') + assert crossmap.coordinate_to_coding(Coord(19), True) == CodingPoint(position=5, offset=0, region='-') + assert crossmap.coordinate_to_coding(Coord(20), True) == CodingPoint(position=6, offset=0, region='-') def test_Coding_no_utr3_degenerate_return(): """A 3' UTR may be missing.""" crossmap = Coding([(10, 20)], (15, 20)) - assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=6, offset=0, region='-') - assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=5, offset=0, region='-') - assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=5, offset=0, region='') - assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=1, offset=0, region='*') + assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=6, offset=0, region='-') + assert crossmap.coordinate_to_coding(Coord(10), True) == CodingPoint(position=5, offset=0, region='-') + assert crossmap.coordinate_to_coding(Coord(19), True) == CodingPoint(position=5, offset=0, region='') + assert crossmap.coordinate_to_coding(Coord(20), True) == CodingPoint(position=1, offset=0, region='*') def test_Coding_no_utr3_inverted_degenerate_return(): """A 3' UTR may be missing.""" - crossmap = Coding([(10, 20)], (15, 20), True) - assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=6, offset=0, region='*') - assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=5, offset=0, region='*') - assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=1, offset=0, region='') - assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=1, offset=0, region='-') + crossmap = Coding([(10, 20)], (15, 20), inverted=True) + + assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=6, offset=0, region='*') + assert crossmap.coordinate_to_coding(Coord(10), True) == CodingPoint(position=5, offset=0, region='*') + assert crossmap.coordinate_to_coding(Coord(19), True) == CodingPoint(position=1, offset=0, region='') + assert crossmap.coordinate_to_coding(Coord(20), True) == CodingPoint(position=1, offset=0, region='-') def test_Coding_small_utr5_degenerate_return(): """A 5' UTR may be of length one.""" crossmap = Coding([(10, 20)], (11, 15)) - assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=2, offset=0, region='-') - assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=1, offset=0, region='-') - assert crossmap.coordinate_to_coding(11, True) == CodingPoint(position=1, offset=0, region='') + assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=2, offset=0, region='-') + assert crossmap.coordinate_to_coding(Coord(10), True) == CodingPoint(position=1, offset=0, region='-') + assert crossmap.coordinate_to_coding(Coord(11), True) == CodingPoint(position=1, offset=0, region='') def test_Coding_small_utr5_inverted_degenerate_return(): """A 5' UTR may be of length one.""" - crossmap = Coding([(10, 20)], (11, 15), True) + crossmap = Coding([(10, 20)], (11, 15), inverted=True) - assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=2, offset=0, region='*') - assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=1, offset=0, region='*') - assert crossmap.coordinate_to_coding(11, True) == CodingPoint(position=4, offset=0, region='') + assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=2, offset=0, region='*') + assert crossmap.coordinate_to_coding(Coord(10), True) == CodingPoint(position=1, offset=0, region='*') + assert crossmap.coordinate_to_coding(Coord(11), True) == CodingPoint(position=4, offset=0, region='') def test_Coding_small_utr3_degenerate_return(): """A 3' UTR may be of length one.""" - crossmap = Coding([(10, 20)], (15, 19)) + crossmap = Coding([(10, 20)], (15, 19), inverted=False) - assert crossmap.coordinate_to_coding(18, True) == CodingPoint(position=4, offset=0, region='') - assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=1, offset=0, region='*') - assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=2, offset=0, region='*') + assert crossmap.coordinate_to_coding(Coord(18), True) == CodingPoint(position=4, offset=0, region='') + assert crossmap.coordinate_to_coding(Coord(19), True) == CodingPoint(position=1, offset=0, region='*') + assert crossmap.coordinate_to_coding(Coord(20), True) == CodingPoint(position=2, offset=0, region='*') def test_Coding_small_utr3_inverted_degenerate_return(): """A 3' UTR may be of length one.""" - crossmap = Coding([(10, 20)], (15, 19), True) + crossmap = Coding([(10, 20)], (15, 19), inverted=True) - assert crossmap.coordinate_to_coding(18, True) == CodingPoint(position=1, offset=0, region='') - assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=1, offset=0, region='-') - assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=2, offset=0, region='-') + assert crossmap.coordinate_to_coding(Coord(18), True) == CodingPoint(position=1, offset=0, region='') + assert crossmap.coordinate_to_coding(Coord(19), True) == CodingPoint(position=1, offset=0, region='-') + assert crossmap.coordinate_to_coding(Coord(20), True) == CodingPoint(position=2, offset=0, region='-') def test_Coding_two_exons_inverted_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" - crossmap = Coding([(10, 20), (30, 40)], (18, 37), True) + crossmap = Coding([(10, 20), (30, 40)], (18, 37), inverted=True) - assert crossmap.coordinate_to_coding(5, True) == CodingPoint(position=13, offset=0, region='*') - assert crossmap.coordinate_to_coding(25, True) == CodingPoint(position=7, offset=5, region='') - assert crossmap.coordinate_to_coding(35, True) == CodingPoint(position=2, offset=0, region='') - assert crossmap.coordinate_to_coding(38, True) == CodingPoint(position=2, offset=0, region='-') + assert crossmap.coordinate_to_coding(Coord(5), True) == CodingPoint(position=13, offset=0, region='*') + assert crossmap.coordinate_to_coding(Coord(25), True) == CodingPoint(position=7, offset=5, region='') + assert crossmap.coordinate_to_coding(Coord(35), True) == CodingPoint(position=2, offset=0, region='') + assert crossmap.coordinate_to_coding(Coord(38), True) == CodingPoint(position=2, offset=0, region='-') def test_Coding_degenerate_no_return(): """Degenerate internal positions do not exist.""" crossmap = Coding([(10, 20), (30, 40)], (10, 40)) - assert crossmap.coordinate_to_coding(25) == crossmap.coordinate_to_coding(25, True) + assert crossmap.coordinate_to_coding(Coord(25)) == crossmap.coordinate_to_coding(Coord(25), True) def test_Coding_inverted_degenerate_no_return(): """Degenerate internal positions do not exist.""" - crossmap = Coding([(10, 20), (30, 40)], (10, 40), True) + crossmap = Coding([(10, 20), (30, 40)], (10, 40), inverted=True) - assert crossmap.coordinate_to_coding(25) == crossmap.coordinate_to_coding(25, True) + assert crossmap.coordinate_to_coding(Coord(25)) == crossmap.coordinate_to_coding(Coord(25), True) def test_Coding_no_utr_degenerate(): @@ -685,34 +626,30 @@ def test_Coding_no_utr_degenerate(): degenerate_equal( crossmap.coding_to_coordinate, - 9, + Coord(9), [ CodingPoint(position=1, offset=-1, region='u'), - # CodingPoint(position=1, offset=0, region='-'), CodingPoint(position=1, offset=-2, region='*'), - CodingPoint(position=1, offset=-1, region=''), CodingPoint(position=1, offset=-1, region='d'), ], ) degenerate_equal( crossmap.coding_to_coordinate, - 11, + Coord(11), [ CodingPoint(position=1, offset=1, region='d'), CodingPoint(position=1, offset=0, region='*'), - # CodingPoint(position=1, offset=2, region='-'), - CodingPoint(position=1, offset=1, region=''), ], ) def test_Coding_inverted_no_utr_degenerate(): """UTRs may be missing.""" - crossmap = Coding([(10, 11)], (10, 11), True) + crossmap = Coding([(10, 11)], (10, 11), inverted=True) degenerate_equal( crossmap.coding_to_coordinate, - 11, + Coord(11), [ CodingPoint(position=1, offset=-1, region='u'), # CodingPoint(position=1, offset=0, region='-'), @@ -723,7 +660,7 @@ def test_Coding_inverted_no_utr_degenerate(): ) degenerate_equal( crossmap.coding_to_coordinate, - 9, + Coord(9), [ CodingPoint(position=1, offset=1, region='d'), CodingPoint(position=1, offset=0, region='*'), @@ -738,34 +675,41 @@ def test_Coding_no_utr_degenerate_return(): """UTRs may be missing.""" crossmap = Coding([(10, 11)], (10, 11)) - assert crossmap.coordinate_to_coding(8, True) == CodingPoint(position=2, offset=0, region='-') - assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=1, offset=0, region='-') - assert crossmap.coordinate_to_coding(11, True) == CodingPoint(position=1, offset=0, region='*') - assert crossmap.coordinate_to_coding(12, True) == CodingPoint(position=2, offset=0, region='*') + assert crossmap.coordinate_to_coding(Coord(8), True) == CodingPoint(position=2, offset=0, region='-') + assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=1, offset=0, region='-') + assert crossmap.coordinate_to_coding(Coord(11), True) == CodingPoint(position=1, offset=0, region='*') + assert crossmap.coordinate_to_coding(Coord(12), True) == CodingPoint(position=2, offset=0, region='*') def test_Coding_inverted_no_utr_degenerate_return(): """UTRs may be missing.""" - crossmap = Coding([(10, 11)], (10, 11), True) + crossmap = Coding([(10, 11)], (10, 11), inverted=True) - assert crossmap.coordinate_to_coding(11, True) == CodingPoint(position=1, offset=0, region='-') - assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=1, offset=0, region='*') + assert crossmap.coordinate_to_coding(Coord(11), True) == CodingPoint(position=1, offset=0, region='-') + assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=1, offset=0, region='*') def test_Coding_protein(): """Protein positions.""" crossmap = Coding(_exons, _cds) + for i in range(0, 80): + t = crossmap.coordinate_to_protein(Coord(i)) + print(f"{i}: {crossmap.coordinate_to_coding(Coord(i))},{t.region} {t.position} {t.offset} {t.position_in_codon}") + + print(crossmap.coordinate_to_protein(Coord(4))) + + print(crossmap.protein_to_coordinate(ProteinPoint(position=4, offset=-1, region='u', position_in_codon=2))) # Boundary between upstream and 5' UTR invariant( crossmap.coordinate_to_protein, - 4, + Coord(4), crossmap.protein_to_coordinate, ProteinPoint(position=4, offset=-1, region='u', position_in_codon=2) ) invariant( crossmap.coordinate_to_protein, - 5, + Coord(5), crossmap.protein_to_coordinate, ProteinPoint(position=4, offset=0, region='-', position_in_codon=2) ) @@ -773,13 +717,13 @@ def test_Coding_protein(): # Boundary between 5' UTR and CDS invariant( crossmap.coordinate_to_protein, - 31, + Coord(31), crossmap.protein_to_coordinate, ProteinPoint(position=1, offset=0, region='-', position_in_codon=3), ) invariant( crossmap.coordinate_to_protein, - 32, + Coord(32), crossmap.protein_to_coordinate, ProteinPoint(position=1, offset=0, region='', position_in_codon=1), ) @@ -787,13 +731,13 @@ def test_Coding_protein(): # Intron boundary. invariant( crossmap.coordinate_to_protein, - 34, + Coord(34), crossmap.protein_to_coordinate, ProteinPoint(position=1, offset=0, region='', position_in_codon=3), ) invariant( crossmap.coordinate_to_protein, - 35, + Coord(35), crossmap.protein_to_coordinate, ProteinPoint(position=1, offset=1, region='', position_in_codon=3), ) @@ -801,13 +745,13 @@ def test_Coding_protein(): # Boundary between CDS and 3' UTR. invariant( crossmap.coordinate_to_protein, - 42, + Coord(42), crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=0, region='', position_in_codon=3), ) invariant( crossmap.coordinate_to_protein, - 43, + Coord(43), crossmap.protein_to_coordinate, ProteinPoint(position=1, offset=0, region='*', position_in_codon=1), ) @@ -815,13 +759,13 @@ def test_Coding_protein(): # Boundary between 3' UTR and downstream invariant( crossmap.coordinate_to_protein, - 71, + Coord(71), crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=0, region='*', position_in_codon=2) ) invariant( crossmap.coordinate_to_protein, - 72, + Coord(72), crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=1, region='d', position_in_codon=2) ) @@ -829,18 +773,18 @@ def test_Coding_protein(): def test_Coding_inverted_protein(): """Protein positions.""" - crossmap = Coding(_exons, _cds, True) + crossmap = Coding(_exons, _cds, inverted = True) # Boundary between upstream and 5' UTR invariant( crossmap.coordinate_to_protein, - 4, + Coord(4), crossmap.protein_to_coordinate, ProteinPoint(position=4, offset=1, region='d', position_in_codon=2) ) invariant( crossmap.coordinate_to_protein, - 5, + Coord(5), crossmap.protein_to_coordinate, ProteinPoint(position=4, offset=0, region='*', position_in_codon=2) ) @@ -848,13 +792,13 @@ def test_Coding_inverted_protein(): # Boundary between 5' UTR and CDS invariant( crossmap.coordinate_to_protein, - 31, + Coord(31), crossmap.protein_to_coordinate, ProteinPoint(position=1, offset=0, region='*', position_in_codon=1), ) invariant( crossmap.coordinate_to_protein, - 32, + Coord(32), crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=0, region='', position_in_codon=3), ) @@ -862,13 +806,13 @@ def test_Coding_inverted_protein(): # Intron boundary. invariant( crossmap.coordinate_to_protein, - 34, + Coord(34), crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=0, region='', position_in_codon=1), ) invariant( crossmap.coordinate_to_protein, - 35, + Coord(35), crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=-1, region='', position_in_codon=1), ) @@ -876,13 +820,13 @@ def test_Coding_inverted_protein(): # Boundary between CDS and 3' UTR. invariant( crossmap.coordinate_to_protein, - 42, + Coord(42), crossmap.protein_to_coordinate, ProteinPoint(position=1, offset=0, region='', position_in_codon=1), ) invariant( crossmap.coordinate_to_protein, - 43, + Coord(43), crossmap.protein_to_coordinate, ProteinPoint(position=1, offset=0, region='-', position_in_codon=3), ) @@ -890,13 +834,13 @@ def test_Coding_inverted_protein(): # Boundary between 3' UTR and downstream invariant( crossmap.coordinate_to_protein, - 71, + Coord(71), crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=0, region='-', position_in_codon=2) ) invariant( crossmap.coordinate_to_protein, - 72, + Coord(72), crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=-1, region='u', position_in_codon=2) ) From 374ae43165086b366e7503883402e1bddad97d26 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 19 Aug 2026 15:21:14 +0200 Subject: [PATCH 178/236] WIP: Add checks in crossmaper and tests. --- mutalyzer_crossmapper/crossmapper.py | 9 +++------ mutalyzer_crossmapper/multi_locus.py | 6 ++---- tests/test_crossmapper.py | 29 ++++++++++++++-------------- 3 files changed, 19 insertions(+), 25 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index d0f55ea..e82930f 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -167,7 +167,6 @@ def __init__( exon_start.position + exon_start.offset, exon_end.position + exon_end.offset + 1 ) - print(f'Coding: {self._coding}, Exons: {self._exons}') def _check_cds(self, cds: tuple[int, int], locations: list[tuple[int, int]], length: int|None = None) -> None: """Check if the CDS is valid.""" @@ -250,6 +249,7 @@ def _coding_to_coordinate(self, point: CodingPoint) -> int: region = point.region position = point.position + # For missing 3' UTR or 5' UTR if region in ('u', 'd'): if region == 'u': if self._coding[0] == self._exons[0]: @@ -283,8 +283,7 @@ def coding_to_coordinate(self, point: CodingPoint) -> int: :returns int: Coordinate. """ - # Check if the point is degenerate - # return self._coding_to_coordinate(point) + # Silently correct for degenerate points region = point.region offset = point.offset position = point.position @@ -293,15 +292,13 @@ def coding_to_coordinate(self, point: CodingPoint) -> int: if position > self._coding[0]: point = CodingPoint(position=self._coding[0], offset=self._coding[0] - position, region='u') if region == '*' and offset == 0: - print("innnnnn") if position > self._exons[1] -self._coding[1]: point = CodingPoint(position=self._exons[1] -self._coding[1], offset=position - (self._exons[1] -self._coding[1]), region='d') - print(f'Coding to coordinate: position={point.position}, offset={point.offset}, region={point.region}') return self._coding_to_coordinate(point) def coordinate_to_protein(self, coord: Coord) -> ProteinPoint: """Convert a coordinate to a protein point model (p.). -point.position + :arg Coord coord: Coordinate model. :returns ProteinPoint: Protein point model(p.). diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index d97fa71..c5cbd9b 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -20,13 +20,11 @@ def __post_init__(self) -> None: def _check_in_range(value: int, length: int | None = None) -> None: """Check if the value no larger than length.""" - print(value, length) if length is not None and value >= length: raise ValueError(f"Value {value} must be within the bounds of the reference sequence {length}.") def _check_multi_locus(locations: list[tuple[int, int]], length: int | None = None) -> None: - print(locations, length) """Check if the locations list is valid.""" for locus in locations: _check_locus(locus) @@ -80,7 +78,6 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - :arg int offset: Offset. :arg str region: Region. """ - print("offsets", self._offsets) # Upstream region validation, position is constant value and offset should not be positive if region == 'u': if position != self._offsets[0]: @@ -109,8 +106,9 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - # '' region validation, position should be within the MultiLocus and offset should not exceed intron length if region == '': + #TODO: should also consider for single locus, where index+1 or index-1 may be out of range if position > self._end-1: - raise IndexError(f"Position {position} exceeds MultiLocus length {self._end}") + raise IndexError(f"Position {position} exceeds multi locus length {self._end}") if offset < 0 and abs(offset) > abs(self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index-1)].boundary[1]): raise IndexError(f"Offset {offset} exceeds intron length.") if offset > 0 and abs(offset) > abs(self._loci[self._direction(index+1)].boundary[0] - self._loci[self._direction(index)].boundary[1]): diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index d6033fa..8ec73af 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -484,20 +484,20 @@ def test_Coding_inverted_degenerate(): CodingPoint(position=1, offset=-1, region='u'), CodingPoint(position=2, offset=0, region='-'), CodingPoint(position=1, offset=-2, region=''), - CodingPoint(position=1, offset=-10, region='*'), - CodingPoint(position=1, offset=-10, region='d'), - CodingPoint(position=2, offset=-3, region=''), + # CodingPoint(position=1, offset=-10, region='*'), + # CodingPoint(position=1, offset=-10, region='d'), + # CodingPoint(position=2, offset=-3, region=''), ], ) degenerate_equal( crossmap.coding_to_coordinate, Coord(9), [ - CodingPoint(position=2, offset=1, region='d'), + CodingPoint(position=1, offset=1, region='d'), CodingPoint(position=2, offset=0, region='*'), - CodingPoint(position=8, offset=2, region=''), - CodingPoint(position=1, offset=10, region='-'), - CodingPoint(position=1, offset=10, region='u'), + # CodingPoint(position=8, offset=2, region=''), + # CodingPoint(position=1, offset=10, region='-'), + # CodingPoint(position=1, offset=10, region='u'), ], ) @@ -629,8 +629,7 @@ def test_Coding_no_utr_degenerate(): Coord(9), [ CodingPoint(position=1, offset=-1, region='u'), - CodingPoint(position=1, offset=-2, region='*'), - CodingPoint(position=1, offset=-1, region='d'), + CodingPoint(position=2, offset=0, region='*'), ], ) degenerate_equal( @@ -652,10 +651,10 @@ def test_Coding_inverted_no_utr_degenerate(): Coord(11), [ CodingPoint(position=1, offset=-1, region='u'), - # CodingPoint(position=1, offset=0, region='-'), - CodingPoint(position=1, offset=-2, region='*'), - CodingPoint(position=1, offset=-1, region=''), - CodingPoint(position=1, offset=-1, region='d'), + CodingPoint(position=1, offset=0, region='-'), + # CodingPoint(position=1, offset=-2, region='*'), + # CodingPoint(position=1, offset=-1, region=''), + # CodingPoint(position=1, offset=-1, region='d'), ], ) degenerate_equal( @@ -665,8 +664,8 @@ def test_Coding_inverted_no_utr_degenerate(): CodingPoint(position=1, offset=1, region='d'), CodingPoint(position=1, offset=0, region='*'), # CodingPoint(position=1, offset=2, region='-'), - CodingPoint(position=1, offset=1, region=''), - CodingPoint(position=1, offset=1, region='u'), + # CodingPoint(position=1, offset=1, region=''), + # CodingPoint(position=1, offset=1, region='u'), ], ) From eff915229eb17f60782e5ff1b34efedaec04531b Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 19 Aug 2026 15:25:35 +0200 Subject: [PATCH 179/236] Fix test. --- tests/test_crossmapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 8ec73af..5a5d322 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -629,7 +629,7 @@ def test_Coding_no_utr_degenerate(): Coord(9), [ CodingPoint(position=1, offset=-1, region='u'), - CodingPoint(position=2, offset=0, region='*'), + CodingPoint(position=1, offset=0, region='-'), ], ) degenerate_equal( From 8f47500b48fc094f2c182b89ad8086f350d01898 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 19 Aug 2026 15:32:57 +0200 Subject: [PATCH 180/236] Fix test. --- tests/test_crossmapper.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 5a5d322..650c0f3 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -483,10 +483,6 @@ def test_Coding_inverted_degenerate(): [ CodingPoint(position=1, offset=-1, region='u'), CodingPoint(position=2, offset=0, region='-'), - CodingPoint(position=1, offset=-2, region=''), - # CodingPoint(position=1, offset=-10, region='*'), - # CodingPoint(position=1, offset=-10, region='d'), - # CodingPoint(position=2, offset=-3, region=''), ], ) degenerate_equal( @@ -495,9 +491,6 @@ def test_Coding_inverted_degenerate(): [ CodingPoint(position=1, offset=1, region='d'), CodingPoint(position=2, offset=0, region='*'), - # CodingPoint(position=8, offset=2, region=''), - # CodingPoint(position=1, offset=10, region='-'), - # CodingPoint(position=1, offset=10, region='u'), ], ) @@ -531,7 +524,7 @@ def test_Coding_no_utr5_degenerate_return(): def test_Coding_no_utr5_inverted_degenerate_return(): """A 5' UTR may be missing.""" - crossmap = Coding([(10, 20)], (15, 20), inverted=True) + crossmap = Coding([(10, 20)], (10, 15), inverted=True) assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=1, offset=0, region='*') assert crossmap.coordinate_to_coding(Coord(10), True) == CodingPoint(position=5, offset=0, region='') From d109d17affdb1f08937005efc6010b6db1039a44 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 19 Aug 2026 15:48:20 +0200 Subject: [PATCH 181/236] Fix typings. --- mutalyzer_crossmapper/crossmapper.py | 10 +++++----- mutalyzer_crossmapper/locus.py | 2 +- mutalyzer_crossmapper/multi_locus.py | 11 ++++++----- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index e82930f..cbc705d 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -239,7 +239,7 @@ def coordinate_to_coding(self, coord: Coord, degenerate: bool = False) -> Coding return CodingPoint(position=position, offset=0, region='*') return point - def _coding_to_coordinate(self, point: CodingPoint) -> int: + def _coding_to_coordinate(self, point: CodingPoint) -> Coord: """Convert a coding position (c./r.) to a coordinate. :arg CodingPoint point: Coding point model (c./r.). @@ -276,12 +276,12 @@ def _coding_to_coordinate(self, point: CodingPoint) -> int: Point(position=position, region='', offset=point.offset) ) - def coding_to_coordinate(self, point: CodingPoint) -> int: + def coding_to_coordinate(self, point: CodingPoint) -> Coord: """Convert a coding position (c./r.) to a coordinate. :arg CodingPoint point: Coding point model (c./r.). - :returns int: Coordinate. + :returns Coord: Coordinate module. """ # Silently correct for degenerate points region = point.region @@ -320,12 +320,12 @@ def coordinate_to_protein(self, coord: Coord) -> ProteinPoint: offset=point.offset ) - def protein_to_coordinate(self, point: ProteinPoint) -> int: + def protein_to_coordinate(self, point: ProteinPoint) -> Coord: """Convert a protein position (p.) to a coordinate. :arg ProteinPoint point: Protein point model(p.). - :returns int: Coordinate. + :returns Coord: Coordinate module. """ if point.region in ('-', 'u'): return self.coding_to_coordinate( diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index ade1fe0..1a27615 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -60,7 +60,7 @@ def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: self.boundary = location[0], location[1] - 1 self._end = location[1] - location[0] # one-based length of the locus - def _validate_point(self, position, offset) -> None: + def _validate_point(self, position: int, offset: int) -> None: """Validate a point model under HGVS rules. :arg int position: Position. diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index c5cbd9b..8cb9ac7 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -4,7 +4,8 @@ from .location import nearest_location -from .locus import Locus, Coord, Point as LocusPoint, _check_locus +from .locus import Locus, Coord, _check_locus +from .locus import Point as LocusPoint @dataclass(slots=True) @@ -65,10 +66,10 @@ def __init__(self, locations: list[tuple[int, int]], length: int |None = None, i self._offsets = _offsets(locations, self._orientation) self._end = sum(end - start for start, end in locations) # one-based length of the MultiLocus - def _validate_coord(self, coord) -> None: + def _validate_coord(self, coordinate:int) -> None: """Check if the coordinate is valid.""" if self._length is not None: - _check_in_range(coord, self._length) + _check_in_range(coordinate, self._length) def _validate_point(self, index: int, position: int, offset: int, region: str) -> None: """Check if a point is valid under HGVS rules. @@ -152,12 +153,12 @@ def to_position(self, coord: Coord) -> Point: region=region, ) - def to_coordinate(self, point: Point) -> int: + def to_coordinate(self, point: Point) -> Coord: """Convert a point model to a coordinate. :arg Point point: Point model. - :returns int: Coordinate. + :returns Coord: Coordinate module. """ index = min( len(self._offsets), From d9977661ec304e502de005a03d7972bb8d66c6b1 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 21 Aug 2026 09:53:21 +0200 Subject: [PATCH 182/236] Fix use PackageMetadata indexing instead of get. --- mutalyzer_crossmapper/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mutalyzer_crossmapper/__init__.py b/mutalyzer_crossmapper/__init__.py index 66f391d..4f4109d 100644 --- a/mutalyzer_crossmapper/__init__.py +++ b/mutalyzer_crossmapper/__init__.py @@ -7,14 +7,15 @@ def _get_metadata(name: str) -> str: - """Get metadata from the package using importlib.metadata""" + """Get metadata from the package using importlib.metadata.""" try: - meta = metadata('mutalyzer_crossmapper') - return meta.get(name, '') + meta = metadata("mutalyzer_crossmapper") + return meta[name] + except KeyError: + return '' except Exception: return '' - _copyright_notice = 'Copyright (c) {} <{}>'.format( _get_metadata('Author'), _get_metadata('Author-email')) From ede43a42d1f666b3640f2216c409364d2a409ac4 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 21 Aug 2026 09:54:58 +0200 Subject: [PATCH 183/236] Remove --strict from mypy. --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9a934f2..51f3d66 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -33,7 +33,7 @@ jobs: - name: Test typings run: | pip install mypy - mypy --strict mutalyzer_crossmapper + mypy mutalyzer_crossmapper - name: Test with pytest run: | pytest From fe5c132dae99b042e06839a783a655a62bed374e Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 24 Aug 2026 12:25:49 +0200 Subject: [PATCH 184/236] Add checks for upstream and downstream region. --- mutalyzer_crossmapper/multi_locus.py | 16 +++++++++++----- tests/test_multi_locus.py | 16 +++++++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 8cb9ac7..96b0872 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -107,13 +107,19 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - # '' region validation, position should be within the MultiLocus and offset should not exceed intron length if region == '': - #TODO: should also consider for single locus, where index+1 or index-1 may be out of range if position > self._end-1: raise IndexError(f"Position {position} exceeds multi locus length {self._end}") - if offset < 0 and abs(offset) > abs(self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index-1)].boundary[1]): - raise IndexError(f"Offset {offset} exceeds intron length.") - if offset > 0 and abs(offset) > abs(self._loci[self._direction(index+1)].boundary[0] - self._loci[self._direction(index)].boundary[1]): - raise IndexError(f"Offset {offset} exceeds intron length.") + if offset < 0: + if index == 0: + raise IndexError(f"Offset {offset} at the first exon should not be in the upstream region.") + if abs(offset) > abs(self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index-1)].boundary[1]): + raise IndexError(f"Offset {offset} exceeds intron length.") + if offset > 0: + if index == len(self._loci): + raise IndexError(f"Offset {offset} at the last exon should not be in the downstream region.") + if abs(offset) > abs(self._loci[self._direction(index+1)].boundary[0] - self._loci[self._direction(index)].boundary[1]): + raise IndexError(f"Offset {offset} exceeds intron length.") + def _direction(self, index: int) -> int: diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index d22d414..a0e4713 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -81,27 +81,33 @@ def test_MultiLocus_invalid_point(): multi_locus = MultiLocus([(5, 10), (15, 20)]) with pytest.raises(ValueError): multi_locus.to_coordinate(Point(position=-5, offset=0, region='')) + with pytest.raises(IndexError): + multi_locus.to_coordinate(Point(position=5, offset=1, region='')) with pytest.raises(IndexError): multi_locus.to_coordinate(Point(position=0, offset=2, region='')) with pytest.raises(IndexError): multi_locus.to_coordinate(Point(position=4, offset=-2, region='')) with pytest.raises(ValueError): multi_locus.to_coordinate(Point(position=2, offset=1, region='')) + with pytest.raises(IndexError): + multi_locus.to_coordinate(Point(position=10, offset=6, region='')) def test_MultiLocus_inverted_invalid_point(): """Reverse orientent MultiLocus with invalid point.""" - multi_locus = MultiLocus([(30, 35), (40, 45)], inverted=True) + multi_locus = MultiLocus([(5, 10), (15, 20)], inverted=True) with pytest.raises(ValueError): multi_locus.to_coordinate(Point(position=-5, offset=0, region='')) with pytest.raises(IndexError): - multi_locus.to_coordinate(Point(position=5, offset=1, region='')) + multi_locus.to_coordinate(Point(position=0, offset=-1, region='')) with pytest.raises(IndexError): - multi_locus.to_coordinate(Point(position=0, offset=2, region='')) + multi_locus.to_coordinate(Point(position=0, offset=-2, region='')) with pytest.raises(IndexError): - multi_locus.to_coordinate(Point(position=4, offset=-2, region='')) + multi_locus.to_coordinate(Point(position=5, offset=2, region='')) with pytest.raises(ValueError): - multi_locus.to_coordinate(Point(position=2, offset=1, region='')) + multi_locus.to_coordinate(Point(position=7, offset=-1, region='')) + with pytest.raises(IndexError): + multi_locus.to_coordinate(Point(position=0, offset=-6, region='')) def test_MultiLocus(): From 47e721b1bab3230a42313e2786d7877d5f6a00bf Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 25 Aug 2026 16:26:54 +0200 Subject: [PATCH 185/236] Add type checks in locus and implement tests for checks in multi locus module. --- mutalyzer_crossmapper/locus.py | 2 +- mutalyzer_crossmapper/multi_locus.py | 76 +++++++++--- tests/helper.py | 5 +- tests/test_multi_locus.py | 177 +++++++++++++++++++++------ 4 files changed, 205 insertions(+), 55 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 1a27615..e7892fb 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -36,7 +36,7 @@ def _check_non_negative_int(value: int) -> None: def _check_locus(locus: tuple[int, int]) -> None: """Check if the range is valid.""" - if len(locus) != 2: + if not isinstance(locus, tuple) or len(locus) != 2: raise ValueError("Locus must be a tuple of two values.") for value in locus: diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 96b0872..1d3f6c9 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -27,6 +27,8 @@ def _check_in_range(value: int, length: int | None = None) -> None: def _check_multi_locus(locations: list[tuple[int, int]], length: int | None = None) -> None: """Check if the locations list is valid.""" + if not locations or not isinstance(locations, list): + raise ValueError("Locations must be a non-empty list of tuples.") for locus in locations: _check_locus(locus) @@ -65,6 +67,7 @@ def __init__(self, locations: list[tuple[int, int]], length: int |None = None, i self._orientation = -1 if inverted else 1 self._offsets = _offsets(locations, self._orientation) self._end = sum(end - start for start, end in locations) # one-based length of the MultiLocus + print(self._offsets) def _validate_coord(self, coordinate:int) -> None: """Check if the coordinate is valid.""" @@ -83,8 +86,8 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - if region == 'u': if position != self._offsets[0]: raise ValueError(f"Position {position} is not at the upstream boundary.") - if offset > 0: - raise ValueError(f"Offset {offset} at upstream boundary should not be positive.") + if offset >= 0: + raise ValueError(f"Offset {offset} at upstream boundary should be negative.") if self._inverted: if self._length is not None and abs(offset) >= self._length - self._loci[self._direction(0)].boundary[1]: raise ValueError(f"Offset {offset} exceeds upstream region.") @@ -96,31 +99,70 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - if region == 'd': if position != self._end-1: raise ValueError(f"Position {position} is not at the downstream boundary {self._end-1}.") - if offset < 0: - raise ValueError(f"Offset {offset} at downstream boundary should not be negative.") + if offset <= 0: + raise ValueError(f"Offset {offset} at downstream boundary should be positive.") if not self._inverted: if self._length is not None and abs(offset) >= self._length - self._loci[self._direction(-1)].boundary[1]: raise ValueError(f"Offset {offset} exceeds downstream region.") else: - if abs(offset) > self._loci[self._direction(0)].boundary[0]: + if abs(offset) >= self._loci[self._direction(0)].boundary[0]: raise ValueError(f"Offset {offset} exceeds downstream boundary.") # '' region validation, position should be within the MultiLocus and offset should not exceed intron length if region == '': - if position > self._end-1: - raise IndexError(f"Position {position} exceeds multi locus length {self._end}") - if offset < 0: - if index == 0: - raise IndexError(f"Offset {offset} at the first exon should not be in the upstream region.") - if abs(offset) > abs(self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index-1)].boundary[1]): - raise IndexError(f"Offset {offset} exceeds intron length.") - if offset > 0: - if index == len(self._loci): - raise IndexError(f"Offset {offset} at the last exon should not be in the downstream region.") - if abs(offset) > abs(self._loci[self._direction(index+1)].boundary[0] - self._loci[self._direction(index)].boundary[1]): - raise IndexError(f"Offset {offset} exceeds intron length.") + if self._inverted: + if offset < 0: + if self._direction(index) == len(self._loci) - 1: + raise ValueError( + f"Offset {offset} at the last exon on the reverse complement should be in the upstream region." + ) + else: + intron_length = abs( + self._loci[self._direction(index-1)].boundary[0] + - self._loci[self._direction(index)].boundary[1] + ) + if abs(offset) >= intron_length: + raise IndexError(f"Offset {offset} exceeds intron length.") + if offset > 0: + if self._direction(index) == 0: + raise ValueError( + f"Offset {offset} at the last exon on the reverse complement should be in the downstream region." + ) + else: + intron_length = abs( + self._loci[self._direction(index)].boundary[0] + - self._loci[self._direction(index+1)].boundary[1] + ) + if abs(offset) >= intron_length: + raise IndexError(f"Offset {offset} exceeds intron length.") + if not self._inverted: + if offset < 0: + if self._direction(index) == 0: + raise ValueError( + f"Offset {offset} at the first exon should be in the upstream region." + ) + else: + intron_length = abs( + self._loci[self._direction(index)].boundary[0] + - self._loci[self._direction(index-1)].boundary[1] + ) + if abs(offset) >= intron_length: + raise IndexError(f"Offset {offset} exceeds intron length.") + if offset > 0: + if self._direction(index) == len(self._loci) - 1: + raise ValueError( + f"Offset {offset} at the last exon should be in the downstream region." + ) + else: + intron_length = abs( + self._loci[self._direction(index+1)].boundary[0] + - self._loci[self._direction(index)].boundary[1] + ) + if abs(offset) >= intron_length: + raise IndexError(f"Offset {offset} exceeds intron length.") + def _direction(self, index: int) -> int: if self._inverted: diff --git a/tests/helper.py b/tests/helper.py index c3205b1..386cbbb 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -1,6 +1,7 @@ def invariant(f, x, f_i, y): - assert f(x) == y - assert f_i(y) == x + args = x if isinstance(x, tuple) else (x,) + assert f(*args) == y + assert f_i(y) == args[0] def degenerate_equal(f, coordinate, locations): diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index a0e4713..13280a8 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -76,40 +76,6 @@ def test_MultiLocus_invalid_coordinate(): multi_locus.to_position(Coord("31")) -def test_MultiLocus_invalid_point(): - """Forward orientent MultiLocus with invalid point.""" - multi_locus = MultiLocus([(5, 10), (15, 20)]) - with pytest.raises(ValueError): - multi_locus.to_coordinate(Point(position=-5, offset=0, region='')) - with pytest.raises(IndexError): - multi_locus.to_coordinate(Point(position=5, offset=1, region='')) - with pytest.raises(IndexError): - multi_locus.to_coordinate(Point(position=0, offset=2, region='')) - with pytest.raises(IndexError): - multi_locus.to_coordinate(Point(position=4, offset=-2, region='')) - with pytest.raises(ValueError): - multi_locus.to_coordinate(Point(position=2, offset=1, region='')) - with pytest.raises(IndexError): - multi_locus.to_coordinate(Point(position=10, offset=6, region='')) - - -def test_MultiLocus_inverted_invalid_point(): - """Reverse orientent MultiLocus with invalid point.""" - multi_locus = MultiLocus([(5, 10), (15, 20)], inverted=True) - with pytest.raises(ValueError): - multi_locus.to_coordinate(Point(position=-5, offset=0, region='')) - with pytest.raises(IndexError): - multi_locus.to_coordinate(Point(position=0, offset=-1, region='')) - with pytest.raises(IndexError): - multi_locus.to_coordinate(Point(position=0, offset=-2, region='')) - with pytest.raises(IndexError): - multi_locus.to_coordinate(Point(position=5, offset=2, region='')) - with pytest.raises(ValueError): - multi_locus.to_coordinate(Point(position=7, offset=-1, region='')) - with pytest.raises(IndexError): - multi_locus.to_coordinate(Point(position=0, offset=-6, region='')) - - def test_MultiLocus(): """Forward oriented MultiLocus.""" multi_locus = MultiLocus(_locations) @@ -499,4 +465,145 @@ def test_one_base_exon_inverted(): Coord(5), multi_locus.to_coordinate, Point(position=0, offset=-1, region='u'), - ) \ No newline at end of file + ) + + +def test_upstream_invalid_position(): + multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=1, offset=-1, region='u')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=-1, offset=-1, region='u')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=20, offset=-1, region='u')) + + +def test_upstream_invalid_position_inverted(): + multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=1, offset=-1, region='u')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=-1, offset=-1, region='u')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=20, offset=-1, region='u')) + + +def test_upstream_invalid_offset(): + multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=0, offset=1, region='u')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=0, offset=-6, region='u')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=0, offset=0, region='u')) + + +def test_upstream_invalid_offset_inverted(): + multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=0, offset=1, region='u')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=0, offset=-6, region='u')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=0, offset=0, region='u')) + + +def test_transcribed_invalid_position(): + multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=-1, offset=0, region='')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=7, offset=1, region='')) + with pytest.raises(IndexError): + multi_locus.to_coordinate(Point(position=20, offset=0, region='')) + + +def test_transcribed_invalid_position_inverted(): + multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=-1, offset=0, region='')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=7, offset=-1, region='')) + with pytest.raises(IndexError): + multi_locus.to_coordinate(Point(position=20, offset=0, region='')) + + +def test_transcribed_invalid_offset(): + multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=0, offset=-5, region='')) + with pytest.raises(IndexError): + multi_locus.to_coordinate(Point(position=0, offset=10, region='')) + with pytest.raises(IndexError): + multi_locus.to_coordinate(Point(position=4, offset=6, region='')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=4, offset=-1, region='')) + with pytest.raises(IndexError): + multi_locus.to_coordinate(Point(position=5, offset=-6, region='')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=5, offset=1, region='')) + + + +def test_transcribed_invalid_offset_inverted(): + multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=9, offset=5, region='')) + with pytest.raises(IndexError): + multi_locus.to_coordinate(Point(position=9, offset=-10, region='')) + with pytest.raises(IndexError): + multi_locus.to_coordinate(Point(position=5, offset=-6, region='')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=5, offset=1, region='')) + with pytest.raises(IndexError): + multi_locus.to_coordinate(Point(position=4, offset=6, region='')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=4, offset=-1, region='')) + + +def test_downstream_invalid_position(): + multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=0, offset=1, region='d')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=-1, offset=1, region='d')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=11, offset=1, region='d')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=100, offset=1, region='d')) + + +def test_downstream_invalid_position_inverted(): + multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=0, offset=1, region='d')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=-1, offset=1, region='d')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=11, offset=1, region='d')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=100, offset=1, region='d')) + + +def test_downstream_invalid_offset(): + multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=9, offset=-1, region='d')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=9, offset=0, region='d')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=9, offset=-5, region='d')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=9, offset=6, region='d')) + + +def test_downstream_invalid_offset_inverted(): + multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=0, offset=1, region='d')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=0, offset=0, region='d')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=0, offset=-5, region='d')) + with pytest.raises(ValueError): + multi_locus.to_coordinate(Point(position=0, offset=6, region='d')) From 452856dd4848d5df3bd25658ab0454afdc48448f Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 26 Aug 2026 09:52:30 +0200 Subject: [PATCH 186/236] Add tests for checks in noncoding and coding. --- mutalyzer_crossmapper/crossmapper.py | 79 +++- mutalyzer_crossmapper/locus.py | 2 +- mutalyzer_crossmapper/multi_locus.py | 13 +- tests/test_crossmapper.py | 674 ++++++++++++++++++++++++--- 4 files changed, 678 insertions(+), 90 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index cbc705d..cc1e8ab 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -1,7 +1,7 @@ from dataclasses import dataclass -from .multi_locus import MultiLocus, Point, Coord, _check_in_range, _check_multi_locus -from .locus import _check_locus, _check_non_negative_int, _check_int +from .multi_locus import MultiLocus, Point, _check_in_range, _check_multi_locus +from .locus import Coord, _check_locus, _check_int from .location import nearest_location @dataclass(slots=True) @@ -10,7 +10,9 @@ class GenomicPoint: position: int def __post_init__(self) -> None: - _check_non_negative_int(self.position) + _check_int(self.position) + if self.position <= 0: + raise ValueError("Genomic position must be a positive integer.") def __str__(self) -> str: return f'{self.position}' @@ -95,13 +97,23 @@ def noncoding_to_coordinate(self, point: NonCodingPoint) -> Coord: :returns Coord: Coordinate model. """ - return self._noncoding.to_coordinate( - Point( - position=point.position - 1, - offset=point.offset, - region=point.region + # Catch errors from multi_locus module + try: + return self._noncoding.to_coordinate( + Point( + position=point.position - 1, + offset=point.offset, + region=point.region + ) ) - ) + except ValueError as e: + if "Position" in str(e): + raise ValueError(str(e).replace(str(point.position - 1), str(point.position))) + raise e + except IndexError as e: + if "Position" in str(e): + raise IndexError(str(e).replace(str(point.position - 1), str(point.position))) + raise e @dataclass(slots=True) @@ -177,6 +189,28 @@ def _check_cds(self, cds: tuple[int, int], locations: list[tuple[int, int]], len if coord < locations[index][0] or coord > locations[index][1]: raise ValueError(f"Coordinate {coord} of CDS {cds} is not within any exon.") + def _validate_point(self, position, region) -> None: + """Validate a coding point model under HGVS rules. + + :arg CodingPoint point: Coding point model. + """ + if region == 'u': + if position not in (1, self._coding[0]): + raise ValueError(f"Position {position} is not in upstream boundary.") + if region == '-': + if position not in range(1, self._coding[0] + 1): + raise ValueError(f"Position {position} exceeds - region.") + if region == '': + if position not in range(1, self._coding[1] - self._coding[0] + 1): + raise ValueError(f"Position {position} exceeds coding region.") + if region == '*': + if position not in range(1, self._exons[1] - self._coding[1] + 1): + raise ValueError(f"Position {position} exceeds * region.") + if region == 'd': + if position not in (1, self._coding[0], self._exons[1] - self._coding[1]): + raise ValueError(f"Position {position} is not in downstream boundary.") + + def _coordinate_to_coding(self, coord: Coord) -> CodingPoint: """Convert a coordinate to a coding point model (c./r.). @@ -249,13 +283,12 @@ def _coding_to_coordinate(self, point: CodingPoint) -> Coord: region = point.region position = point.position + self._validate_point(position, region) + # For missing 3' UTR or 5' UTR if region in ('u', 'd'): if region == 'u': - if self._coding[0] == self._exons[0]: - position = 1 - else: - position = 1 + position = 1 if region == 'd': if self._coding[1] == self._exons[1]: position = self._coding[1] @@ -265,13 +298,13 @@ def _coding_to_coordinate(self, point: CodingPoint) -> Coord: Point(position=position - 1, region=point.region, offset=point.offset) ) - if region == '': position = position + self._coding[0] - 1 elif region == '-': position = self._coding[0] - position elif region == '*': position = self._coding[1] + position - 1 + return self._noncoding.to_coordinate( Point(position=position, region='', offset=point.offset) ) @@ -284,16 +317,16 @@ def coding_to_coordinate(self, point: CodingPoint) -> Coord: :returns Coord: Coordinate module. """ # Silently correct for degenerate points - region = point.region - offset = point.offset - position = point.position + if point.offset == 0: + if point.region == '-' and point.position > self._coding[0]: + if self._coding[0] == 0: + return self._coding_to_coordinate(CodingPoint(position=1, offset=self._coding[0] - point.position, region='u')) + return self._coding_to_coordinate(CodingPoint(position=self._coding[0], offset=self._coding[0] - point.position, region='u')) + if point.region == '*' and point.position > self._exons[1] - self._coding[1]: + if self._exons[1] == self._coding[1]: + return self._coding_to_coordinate(CodingPoint(position=1, offset=point.position - (self._exons[1] - self._coding[1]), region='d')) + return self._coding_to_coordinate(CodingPoint(position=self._exons[1] - self._coding[1], offset=point.position - (self._exons[1] - self._coding[1]), region='d')) - if region == '-' and offset == 0: - if position > self._coding[0]: - point = CodingPoint(position=self._coding[0], offset=self._coding[0] - position, region='u') - if region == '*' and offset == 0: - if position > self._exons[1] -self._coding[1]: - point = CodingPoint(position=self._exons[1] -self._coding[1], offset=position - (self._exons[1] -self._coding[1]), region='d') return self._coding_to_coordinate(point) def coordinate_to_protein(self, coord: Coord) -> ProteinPoint: diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index e7892fb..390c1a1 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -73,7 +73,7 @@ def _validate_point(self, position: int, offset: int) -> None: if offset > 0 and position != self._end-1: raise IndexError(f"Offset {offset} should be at a locus end.") if position > self._end-1: - raise IndexError(f"Position {position} exceeds locus length {self._end}") + raise IndexError(f"Position {position} exceeds locus length.") def to_position(self, coord: Coord) -> Point: """Convert a coordinate to a proper point model. diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 1d3f6c9..ef89e1a 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -67,7 +67,6 @@ def __init__(self, locations: list[tuple[int, int]], length: int |None = None, i self._orientation = -1 if inverted else 1 self._offsets = _offsets(locations, self._orientation) self._end = sum(end - start for start, end in locations) # one-based length of the MultiLocus - print(self._offsets) def _validate_coord(self, coordinate:int) -> None: """Check if the coordinate is valid.""" @@ -85,20 +84,20 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - # Upstream region validation, position is constant value and offset should not be positive if region == 'u': if position != self._offsets[0]: - raise ValueError(f"Position {position} is not at the upstream boundary.") + raise ValueError(f"Position {position} is not at upstream boundary.") if offset >= 0: raise ValueError(f"Offset {offset} at upstream boundary should be negative.") if self._inverted: if self._length is not None and abs(offset) >= self._length - self._loci[self._direction(0)].boundary[1]: raise ValueError(f"Offset {offset} exceeds upstream region.") else: - if abs(offset) > self._loci[self._direction(0)].boundary[0]: + if abs(offset) >= self._loci[self._direction(0)].boundary[0]: raise ValueError(f"Offset {offset} exceeds upstream boundary.") # Downstream region validation, position is constant value and offset should not be negative if region == 'd': if position != self._end-1: - raise ValueError(f"Position {position} is not at the downstream boundary {self._end-1}.") + raise ValueError(f"Position {position} is not at downstream boundary.") if offset <= 0: raise ValueError(f"Offset {offset} at downstream boundary should be positive.") if not self._inverted: @@ -144,10 +143,12 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - f"Offset {offset} at the first exon should be in the upstream region." ) else: + print("boundary", self._loci[self._direction(index)].boundary, self._loci[self._direction(index-1)].boundary) intron_length = abs( self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index-1)].boundary[1] ) + print("intron length", intron_length, "position", position, "offset", offset) if abs(offset) >= intron_length: raise IndexError(f"Offset {offset} exceeds intron length.") if offset > 0: @@ -233,12 +234,12 @@ def to_coordinate(self, point: Point) -> Coord: except ValueError as e: if "Position" in str(e): - raise ValueError(f"Position {point.position} is not at a locus boundary.") from e + raise ValueError(str(e).replace(str(point.position - 1), str(point.position))) from e raise e except IndexError as e: if "Position" in str(e): raise IndexError( - f"Position {point.position} exceeds multi_locus length {self._end}" + f"Position {point.position} exceeds multi locus length." ) from e raise e diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 650c0f3..bf10b39 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -7,6 +7,16 @@ _cds = (32, 43) +def test_GenomicPoint_invalid_initialization(): + """GenomicPoint cannot be initialized with invalid position.""" + with pytest.raises(ValueError) as e: + GenomicPoint(position=0) + with pytest.raises(ValueError) as e: + GenomicPoint(position=-1) + with pytest.raises(ValueError) as e: + GenomicPoint(position=[101]) + + def test_Genomic(): """Genomic positions are coordinates incremented by one.""" crossmap = Genomic() @@ -25,6 +35,84 @@ def test_Genomic(): ) +def test_Genomic_invalid_with_length(): + """Raise ValueError if coordinate is out of bounds.""" + crossmap = Genomic() + with pytest.raises(ValueError) as e: + crossmap.coordinate_to_genomic(Coord(-1), 99) + with pytest.raises(ValueError) as e: + crossmap.coordinate_to_genomic(Coord(99), 99) + + +def test_Genomic_with_length(): + """Genomic positions are coordinates incremented by one.""" + crossmap = Genomic() + + invariant( + crossmap.coordinate_to_genomic, + (Coord(0), 99), + crossmap.genomic_to_coordinate, + GenomicPoint(position=1), + ) + invariant( + crossmap.coordinate_to_genomic, + (Coord(98), 99), + crossmap.genomic_to_coordinate, + GenomicPoint(position=99), + ) + + +def test_NonCodingPoint_invalid_initialization(): + """Raise error with invalid initialization.""" + with pytest.raises(ValueError) as e: + NonCodingPoint(position=0, offset=0, region='u') + with pytest.raises(ValueError) as e: + NonCodingPoint(position=0, offset=0, region='d') + with pytest.raises(ValueError) as e: + NonCodingPoint(position=0, offset=0, region='') + with pytest.raises(ValueError) as e: + NonCodingPoint(position=0, offset=0, region='*') + with pytest.raises(ValueError) as e: + NonCodingPoint(position=-1, offset=0, region='') + with pytest.raises(ValueError) as e: + NonCodingPoint(position=1, offset=None, region='u') + + +def test_NonCoding_invalid(): + """Raise ValueError if noncoding is invalid.""" + with pytest.raises(ValueError) as e: + NonCoding([()]) + with pytest.raises(ValueError) as e: + NonCoding([(10)]) + with pytest.raises(ValueError) as e: + NonCoding([(10, 20), (15, 25)]) + with pytest.raises(ValueError) as e: + NonCoding([(None, 20), (30, None)]) + with pytest.raises(ValueError) as e: + NonCoding(_exons, length=70) + + # Reverse orientation + with pytest.raises(ValueError) as e: + NonCoding([()], inverted=True) + with pytest.raises(ValueError) as e: + NonCoding([(10)], inverted=True) + with pytest.raises(ValueError) as e: + NonCoding([(10, 20), (15, 25)], inverted=True) + with pytest.raises(ValueError) as e: + NonCoding([(None, 20), (30, None)], inverted=True) + with pytest.raises(ValueError) as e: + NonCoding(_exons, length=70, inverted=True) + + +def test_NonCoding_invalid_with_length(): + """Raise ValueError if coordinate is out of bounds.""" + with pytest.raises(ValueError) as e: + NonCoding(_exons, length=70) + # Reverse orientation + with pytest.raises(ValueError) as e: + NonCoding(_exons, length=70, inverted=True) + + def test_NonCoding(): """Forward oriented noncoding transcript.""" crossmap = NonCoding(_exons) @@ -64,8 +152,59 @@ def test_NonCoding(): ) +def test_NonCoding_with_length(): + """Raise ValueError if coordinate is out of bounds.""" + crossmap = NonCoding(_exons, length=75) + + # Boundary between upstream and transcript. + invariant( + crossmap.coordinate_to_noncoding, + Coord(3) , + crossmap.noncoding_to_coordinate, + NonCodingPoint(position=1, offset=-2, region='u'), + ) + invariant( + crossmap.coordinate_to_noncoding, + Coord(4), + crossmap.noncoding_to_coordinate, + NonCodingPoint(position=1, offset=-1, region='u'), + ) + invariant( + crossmap.coordinate_to_noncoding, + Coord(5), + crossmap.noncoding_to_coordinate, + NonCodingPoint(position=1, offset=0, region=''), + ) + + # Boundary between downstream and transcript. + invariant( + crossmap.coordinate_to_noncoding, + Coord(71), + crossmap.noncoding_to_coordinate, + NonCodingPoint(position=22, offset=0, region=''), + ) + invariant( + crossmap.coordinate_to_noncoding, + Coord(72), + crossmap.noncoding_to_coordinate, + NonCodingPoint(position=22, offset=1, region='d'), + ) + + # Boundary between downstream and sequence end. + invariant( + crossmap.coordinate_to_noncoding, + Coord(74), + crossmap.noncoding_to_coordinate, + NonCodingPoint(position=22, offset=3, region='d'), + ) + with pytest.raises(ValueError) as e: + crossmap.coordinate_to_noncoding(Coord(75)) + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=4, region='d')) + + def test_NonCoding_inverted(): - """Forward oriented noncoding transcript.""" + """Reverse oriented noncoding transcript.""" crossmap = NonCoding(_exons, inverted=True) # Boundary between upstream and transcript. @@ -97,11 +236,227 @@ def test_NonCoding_inverted(): ) +def test_NonCoding_inverted_with_length(): + """Reverse oriented noncoding transcript.""" + crossmap = NonCoding(_exons, length=75, inverted=True) + + # Boundary between upstream and sequence end. + with pytest.raises(ValueError) as e: + crossmap.coordinate_to_noncoding(Coord(75)) + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-4, region='u')) + invariant( + crossmap.coordinate_to_noncoding, + Coord(74), + crossmap.noncoding_to_coordinate, + NonCodingPoint(position=1, offset=-3, region='u'), + ) + + # Boundary between upstream and transcript. + invariant( + crossmap.coordinate_to_noncoding, + Coord(72), + crossmap.noncoding_to_coordinate, + NonCodingPoint(position=1, offset=-1, region='u'), + ) + invariant( + crossmap.coordinate_to_noncoding, + Coord(71), + crossmap.noncoding_to_coordinate, + NonCodingPoint(position=1, offset=0, region=''), + ) + + # Boundary between downstream and transcript. + invariant( + crossmap.coordinate_to_noncoding, + Coord(5), + crossmap.noncoding_to_coordinate, + NonCodingPoint(position=22, offset=0, region=''), + ) + invariant( + crossmap.coordinate_to_noncoding, + Coord(4), + crossmap.noncoding_to_coordinate, + NonCodingPoint(position=22, offset=1, region='d'), + ) + + +def test_NonCoding_invalid_position(): + """Raise error if position is not valid under HGVS rules.""" + crossmap = NonCoding(_exons, length=75) + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=0, offset=1, region='u')) + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=2, offset=1, region='u')) + with pytest.raises(IndexError): + crossmap.noncoding_to_coordinate(NonCodingPoint(position=23, offset=0, region='')) + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=21, offset=1, region='d')) + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=30, offset=-1, region='d')) + + +def test_NonCoding_invalid_position_inverted(): + """Raise error if position is not valid under HGVS rules.""" + crossmap = NonCoding(_exons, length=75, inverted=True) + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=0, offset=1, region='u')) + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=2, offset=1, region='u')) + with pytest.raises(IndexError): + crossmap.noncoding_to_coordinate(NonCodingPoint(position=23, offset=0, region='')) + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=21, offset=1, region='d')) + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=30, offset=-1, region='d')) + + +def test_NonCoding_invalid_offset(): + """Raise error if offset is not valid under HGVS rules.""" + crossmap = NonCoding(_exons, length=75) + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=0, region='u')) + assert e.value.args[0] == "Offset 0 at upstream boundary should be negative." + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=1, region='u')) + assert e.value.args[0] == "Offset 1 at upstream boundary should be negative." + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-5, region='u')) + assert e.value.args[0] == "Offset -5 exceeds upstream boundary." + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-1, region='')) + assert e.value.args[0] == "Offset -1 at the first exon should be in the upstream region." + with pytest.raises(IndexError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=1, region='')) + assert e.value.args[0] == "Offset 1 should be at a locus end." + with pytest.raises(IndexError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=10, offset=1, region='')) + assert e.value.args[0] == "Offset 1 should be at a locus end." + with pytest.raises(IndexError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=10, offset=-11, region='')) + assert e.value.args[0] == "Offset -11 exceeds intron length." + with pytest.raises(IndexError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=-1, region='')) + assert e.value.args[0] == "Offset -1 should be at a locus start." + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=1, region='')) + assert e.value.args[0] == "Offset 1 at the first exon should be in the downstream region." + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=0, region='d')) + assert e.value.args[0] == "Offset 0 at downstream boundary should be positive." + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=-1, region='d')) + assert e.value.args[0] == "Offset -1 at downstream boundary should be positive." + + +def test_NonCoding_invalid_offset_inverted(): + """Raise error if offset is not valid under HGVS rules.""" + crossmap = NonCoding(_exons, length=75, inverted=True) + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=0, region='u')) + assert e.value.args[0] == "Offset 0 at upstream boundary should be negative." + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=1, region='u')) + assert e.value.args[0] == "Offset 1 at upstream boundary should be negative." + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-5, region='u')) + assert e.value.args[0] == "Offset -5 exceeds upstream boundary." + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-1, region='')) + assert e.value.args[0] == "Offset -1 at the first exon should be in the upstream region." + with pytest.raises(IndexError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=1, region='')) + assert e.value.args[0] == "Offset 1 should be at a locus end." + with pytest.raises(IndexError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=13, offset=-1, region='')) + assert e.value.args[0] == "Offset -1 should be at a locus end." + with pytest.raises(IndexError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=13, offset=11, region='')) + assert e.value.args[0] == "Offset 11 exceeds intron length." + with pytest.raises(IndexError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=-1, region='')) + assert e.value.args[0] == "Offset -1 should be at a locus start." + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=1, region='')) + assert e.value.args[0] == "Offset 1 at the last exon on the reverse complement should be in the downstream region." + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=0, region='d')) + assert e.value.args[0] == "Offset 0 at downstream boundary should be positive." + with pytest.raises(ValueError) as e: + crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=-1, region='d')) + assert e.value.args[0] == "Offset -1 at downstream boundary should be positive." + + +def test_CodingPoint_invalid_initialization(): + """Raise error with invalid initialization.""" + with pytest.raises(ValueError) as e: + CodingPoint(position=0, offset=0, region='-') + with pytest.raises(ValueError) as e: + CodingPoint(position=0, offset=0, region='*') + with pytest.raises(ValueError) as e: + CodingPoint(position=0, offset=0, region='') + with pytest.raises(ValueError) as e: + CodingPoint(position=-1, offset=0, region='') + with pytest.raises(ValueError) as e: + CodingPoint(position=1, offset=None, region='') + with pytest.raises(ValueError) as e: + CodingPoint(position=2, offset=1, region='upstream') + + +def test_Coding_invalid(): + """Raise ValueError if coding is invalid.""" + + with pytest.raises(ValueError) as e: + Coding([(20, 20)], (20, 20)) + with pytest.raises(ValueError) as e: + Coding([(10, 20)], (9,15)) + with pytest.raises(ValueError) as e: + Coding([(10, 20)], (10,21)) + with pytest.raises(ValueError) as e: + Coding([(10, 20)], (15, 10)) + with pytest.raises(ValueError) as e: + Coding([], None) + + # Reverse orientation + with pytest.raises(ValueError) as e: + Coding([(20, 20)], (20, 20), inverted=True) + with pytest.raises(ValueError) as e: + Coding([(10, 20)], (9,15), inverted=True) + with pytest.raises(ValueError) as e: + Coding([(10, 20)], (10,21), inverted=True) + with pytest.raises(ValueError) as e: + Coding([(10, 20)], (15, 10), inverted=True) + with pytest.raises(ValueError) as e: + Coding([], None, inverted=True) + + +def test_Coding_invalid_with_length(): + """Raise ValueError if coordinate is out of bounds.""" + with pytest.raises(ValueError) as e: + Coding(_exons, _cds, length=70) + # Reverse orientation + with pytest.raises(ValueError) as e: + Coding(_exons, _cds, length=70, inverted=True) + def test_Coding(): """Forward oriented coding transcript.""" crossmap = Coding(_exons, _cds) + # Boundary between upstream and 5' UTR. + invariant( + crossmap.coordinate_to_coding, + Coord(4), + crossmap.coding_to_coordinate, + CodingPoint(position=11, offset=-1, region='u'), + ) + invariant( + crossmap.coordinate_to_coding, + Coord(5), + crossmap.coding_to_coordinate, + CodingPoint(position=11, offset=0, region='-'), + ) + # Boundary between 5' and CDS. invariant( crossmap.coordinate_to_coding, @@ -130,11 +485,111 @@ def test_Coding(): CodingPoint(position=1, offset=0, region='*'), ) + # Boundary between 3' and downstream. + invariant( + crossmap.coordinate_to_coding, + Coord(71), + crossmap.coding_to_coordinate, + CodingPoint(position=5, offset=0, region='*'), + ) + invariant( + crossmap.coordinate_to_coding, + Coord(72), + crossmap.coding_to_coordinate, + CodingPoint(position=5, offset=1, region='d'), + ) + + +def test_Coding_with_length(): + """Raise ValueError if coordinate is out of bounds.""" + crossmap = Coding(_exons, _cds, length=75) + # Boundary between upstream and 5' UTR. + invariant( + crossmap.coordinate_to_coding, + Coord(4), + crossmap.coding_to_coordinate, + CodingPoint(position=11, offset=-1, region='u'), + ) + invariant( + crossmap.coordinate_to_coding, + Coord(5), + crossmap.coding_to_coordinate, + CodingPoint(position=11, offset=0, region='-'), + ) + + # Boundary between 5' and CDS. + invariant( + crossmap.coordinate_to_coding, + Coord(31), + crossmap.coding_to_coordinate, + CodingPoint(position=1, offset=0, region='-'), + ) + invariant( + crossmap.coordinate_to_coding, + Coord(32), + crossmap.coding_to_coordinate, + CodingPoint(position=1, offset=0, region=''), + ) + + # Boundary between CDS and 3'. + invariant( + crossmap.coordinate_to_coding, + Coord(42), + crossmap.coding_to_coordinate, + CodingPoint(position=6, offset=0, region=''), + ) + invariant( + crossmap.coordinate_to_coding, + Coord(43), + crossmap.coding_to_coordinate, + CodingPoint(position=1, offset=0, region='*'), + ) + + # Boundary between 3' and downstream. + invariant( + crossmap.coordinate_to_coding, + Coord(71), + crossmap.coding_to_coordinate, + CodingPoint(position=5, offset=0, region='*'), + ) + invariant( + crossmap.coordinate_to_coding, + Coord(72), + crossmap.coding_to_coordinate, + CodingPoint(position=5, offset=1, region='d'), + ) + + # Boundary between downstream and sequence end. + invariant( + crossmap.coordinate_to_coding, + Coord(74), + crossmap.coding_to_coordinate, + CodingPoint(position=5, offset=3, region='d'), + ) + with pytest.raises(ValueError) as e: + crossmap.coordinate_to_coding(Coord(75)) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=5, offset=4, region='d')) + def test_Coding_inverted(): """Reverse oriented coding transcript.""" crossmap = Coding(_exons, _cds, inverted=True) + # Boundary between upstream and 5' UTR. + invariant( + crossmap.coordinate_to_coding, + Coord(72), + crossmap.coding_to_coordinate, + CodingPoint(position=5, offset=-1, region='u'), + ) + invariant( + crossmap.coordinate_to_coding, + Coord(71), + crossmap.coding_to_coordinate, + CodingPoint(position=5, offset=0, region='-'), + ) + # Boundary between 5' and CDS. invariant( crossmap.coordinate_to_coding, @@ -163,6 +618,93 @@ def test_Coding_inverted(): CodingPoint(position=1, offset=0, region='*'), ) + # Boundary between 3' and downstream. + invariant( + crossmap.coordinate_to_coding, + Coord(5), + crossmap.coding_to_coordinate, + CodingPoint(position=11, offset=0, region='*'), + ) + invariant( + crossmap.coordinate_to_coding, + Coord(4), + crossmap.coding_to_coordinate, + CodingPoint(position=11, offset=1, region='d'), + ) + + +def test_Coding_inverted_with_length(): + """Reverse oriented coding transcript.""" + crossmap = Coding(_exons, _cds, length=75, inverted=True) + + # Boundary between upstream and sequence end. + with pytest.raises(ValueError) as e: + crossmap.coordinate_to_coding(Coord(75)) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=5, offset=-4, region='u')) + invariant( + crossmap.coordinate_to_coding, + Coord(74), + crossmap.coding_to_coordinate, + CodingPoint(position=5, offset=-3, region='u'), + ) + + # Boundary between upstream and 5' UTR. + invariant( + crossmap.coordinate_to_coding, + Coord(72), + crossmap.coding_to_coordinate, + CodingPoint(position=5, offset=-1, region='u'), + ) + invariant( + crossmap.coordinate_to_coding, + Coord(71), + crossmap.coding_to_coordinate, + CodingPoint(position=5, offset=0, region='-'), + ) + + # Boundary between 5' and CDS. + invariant( + crossmap.coordinate_to_coding, + Coord(43), + crossmap.coding_to_coordinate, + CodingPoint(position=1, offset=0, region='-'), + ) + invariant( + crossmap.coordinate_to_coding, + Coord(42), + crossmap.coding_to_coordinate, + CodingPoint(position=1, offset=0, region=''), + ) + + # Boundary between CDS and 3'. + invariant( + crossmap.coordinate_to_coding, + Coord(32), + crossmap.coding_to_coordinate, + CodingPoint(position=6, offset=0, region=''), + ) + invariant( + crossmap.coordinate_to_coding, + Coord(31), + crossmap.coding_to_coordinate, + CodingPoint(position=1, offset=0, region='*'), + ) + + # Boundary between 3' and downstream. + invariant( + crossmap.coordinate_to_coding, + Coord(5), + crossmap.coding_to_coordinate, + CodingPoint(position=11, offset=0, region='*'), + ) + invariant( + crossmap.coordinate_to_coding, + Coord(4), + crossmap.coding_to_coordinate, + CodingPoint(position=11, offset=1, region='d'), + ) + def test_Coding_regions(): """The CDS can start or end on a region boundary.""" @@ -249,50 +791,6 @@ def test_Coding_no_utr5(): ) -def test_Coding_no_intron(): - crossmap = Coding([(10, 20), (20, 30)], (15, 25)) - - invariant( - crossmap.coordinate_to_coding, - Coord(20), - crossmap.coding_to_coordinate, - CodingPoint(position=6, offset=0, region=''), - ) - - -def test_Coding_no_intron_inverted(): - crossmap = Coding([(10, 20), (20, 30)], (15, 25), inverted=True) - - invariant( - crossmap.coordinate_to_coding, - Coord(20), - crossmap.coding_to_coordinate, - CodingPoint(position=5, offset=0, region=''), - ) - - -def test_Coding_one_base_intron(): - crossmap = Coding([(10, 19), (20, 30)], (15, 25)) - - invariant( - crossmap.coordinate_to_coding, - Coord(19), - crossmap.coding_to_coordinate, - CodingPoint(position=4, offset=1, region=''), - ) - - -def test_Coding_one_base_intron_inverted(): - crossmap = Coding([(10, 19), (20, 30)], (15, 25), inverted=True) - - invariant( - crossmap.coordinate_to_coding, - Coord(19), - crossmap.coding_to_coordinate, - CodingPoint(position=5, offset=1, region=''), - ) - - def test_Coding_no_utr5_inverted(): """A 5' UTR may be missing.""" crossmap = Coding([(10, 20)], (15, 20), inverted=True) @@ -315,8 +813,6 @@ def test_Coding_no_utr5_inverted(): def test_Coding_no_utr3(): """A 3' UTR may be missing.""" crossmap = Coding([(10, 20)], (15, 20)) - print(crossmap.coordinate_to_coding(Coord(20)).position, crossmap.coordinate_to_coding(Coord(20)).offset, crossmap.coordinate_to_coding(Coord(20)).region) - print(crossmap.coding_to_coordinate(CodingPoint(position=5, offset=1, region='d'))) # Direct transition from CDS to downstream. invariant( crossmap.coordinate_to_coding, @@ -450,6 +946,50 @@ def test_Coding_small_utr3_inverted(): ) +def test_Coding_no_intron(): + crossmap = Coding([(10, 20), (20, 30)], (15, 25)) + + invariant( + crossmap.coordinate_to_coding, + Coord(20), + crossmap.coding_to_coordinate, + CodingPoint(position=6, offset=0, region=''), + ) + + +def test_Coding_no_intron_inverted(): + crossmap = Coding([(10, 20), (20, 30)], (15, 25), inverted=True) + + invariant( + crossmap.coordinate_to_coding, + Coord(20), + crossmap.coding_to_coordinate, + CodingPoint(position=5, offset=0, region=''), + ) + + +def test_Coding_one_base_intron(): + crossmap = Coding([(10, 19), (20, 30)], (15, 25)) + + invariant( + crossmap.coordinate_to_coding, + Coord(19), + crossmap.coding_to_coordinate, + CodingPoint(position=4, offset=1, region=''), + ) + + +def test_Coding_one_base_intron_inverted(): + crossmap = Coding([(10, 19), (20, 30)], (15, 25), inverted=True) + + invariant( + crossmap.coordinate_to_coding, + Coord(19), + crossmap.coding_to_coordinate, + CodingPoint(position=5, offset=1, region=''), + ) + + def test_Coding_degenerate(): """Degenerate upstream and downstream positions are silently corrected.""" crossmap = Coding([(10, 20)], (11, 19)) @@ -645,9 +1185,6 @@ def test_Coding_inverted_no_utr_degenerate(): [ CodingPoint(position=1, offset=-1, region='u'), CodingPoint(position=1, offset=0, region='-'), - # CodingPoint(position=1, offset=-2, region='*'), - # CodingPoint(position=1, offset=-1, region=''), - # CodingPoint(position=1, offset=-1, region='d'), ], ) degenerate_equal( @@ -656,9 +1193,6 @@ def test_Coding_inverted_no_utr_degenerate(): [ CodingPoint(position=1, offset=1, region='d'), CodingPoint(position=1, offset=0, region='*'), - # CodingPoint(position=1, offset=2, region='-'), - # CodingPoint(position=1, offset=1, region=''), - # CodingPoint(position=1, offset=1, region='u'), ], ) @@ -680,17 +1214,37 @@ def test_Coding_inverted_no_utr_degenerate_return(): assert crossmap.coordinate_to_coding(Coord(11), True) == CodingPoint(position=1, offset=0, region='-') assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=1, offset=0, region='*') +_exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] +_cds = (32, 43) +def test_Coding_invalid_position(): + """Raise error if position in coding point is invalid.""" + crossmap = Coding(_exons, _cds) + + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=0, offset=1, region='u')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=12, offset=-1, region='u')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=13, offset=1, region='-')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=-1, offset=0, region='-')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=0, offset=0, region='')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=7, offset=0, region='')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=6, offset=1, region='*')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=None, offset=0, region='*')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=6, offset=0, region='d')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=1000, offset=2, region='d')) + def test_Coding_protein(): """Protein positions.""" crossmap = Coding(_exons, _cds) - for i in range(0, 80): - t = crossmap.coordinate_to_protein(Coord(i)) - print(f"{i}: {crossmap.coordinate_to_coding(Coord(i))},{t.region} {t.position} {t.offset} {t.position_in_codon}") - - print(crossmap.coordinate_to_protein(Coord(4))) - - print(crossmap.protein_to_coordinate(ProteinPoint(position=4, offset=-1, region='u', position_in_codon=2))) # Boundary between upstream and 5' UTR invariant( From 946e7c4f1d8ae84f9acb6f65fef623a20ee7c11c Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 26 Aug 2026 11:35:41 +0200 Subject: [PATCH 187/236] Fix offset checks in upstream and downstream. --- mutalyzer_crossmapper/multi_locus.py | 6 ++---- tests/test_crossmapper.py | 30 ++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index ef89e1a..a25f14c 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -91,7 +91,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - if self._length is not None and abs(offset) >= self._length - self._loci[self._direction(0)].boundary[1]: raise ValueError(f"Offset {offset} exceeds upstream region.") else: - if abs(offset) >= self._loci[self._direction(0)].boundary[0]: + if abs(offset) > self._loci[self._direction(0)].boundary[0]: raise ValueError(f"Offset {offset} exceeds upstream boundary.") # Downstream region validation, position is constant value and offset should not be negative @@ -104,7 +104,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - if self._length is not None and abs(offset) >= self._length - self._loci[self._direction(-1)].boundary[1]: raise ValueError(f"Offset {offset} exceeds downstream region.") else: - if abs(offset) >= self._loci[self._direction(0)].boundary[0]: + if abs(offset) >= self._loci[self._direction(len(self._locations))].boundary[0]: raise ValueError(f"Offset {offset} exceeds downstream boundary.") # '' region validation, position should be within the MultiLocus and offset should not exceed intron length @@ -143,12 +143,10 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - f"Offset {offset} at the first exon should be in the upstream region." ) else: - print("boundary", self._loci[self._direction(index)].boundary, self._loci[self._direction(index-1)].boundary) intron_length = abs( self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index-1)].boundary[1] ) - print("intron length", intron_length, "position", position, "offset", offset) if abs(offset) >= intron_length: raise IndexError(f"Offset {offset} exceeds intron length.") if offset > 0: diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index bf10b39..6ef3f51 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -321,8 +321,8 @@ def test_NonCoding_invalid_offset(): crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=1, region='u')) assert e.value.args[0] == "Offset 1 at upstream boundary should be negative." with pytest.raises(ValueError) as e: - crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-5, region='u')) - assert e.value.args[0] == "Offset -5 exceeds upstream boundary." + crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-6, region='u')) + assert e.value.args[0] == "Offset -6 exceeds upstream boundary." with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-1, region='')) assert e.value.args[0] == "Offset -1 at the first exon should be in the upstream region." @@ -1242,6 +1242,32 @@ def test_Coding_invalid_position(): crossmap.coding_to_coordinate(CodingPoint(position=1000, offset=2, region='d')) +def test_Coding_inverted_invalid_position_inverted(): + """Raise error if position in coding point is invalid for inverted coding.""" + crossmap = Coding(_exons, _cds, inverted=True) + + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=0, offset=1, region='u')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=12, offset=-1, region='u')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=13, offset=1, region='-')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=-1, offset=0, region='-')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=0, offset=0, region='')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=7, offset=0, region='')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=6, offset=1, region='*')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=None, offset=0, region='*')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=6, offset=0, region='d')) + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=1000, offset=2, region='d')) + + def test_Coding_protein(): """Protein positions.""" crossmap = Coding(_exons, _cds) From 57ae3ccaffc2566d920b9c63e11d6fedbd9c7ba1 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 26 Aug 2026 15:35:49 +0200 Subject: [PATCH 188/236] Fix offset checks in upstream and downstream. --- mutalyzer_crossmapper/multi_locus.py | 2 +- tests/test_multi_locus.py | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index a25f14c..229567a 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -104,7 +104,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - if self._length is not None and abs(offset) >= self._length - self._loci[self._direction(-1)].boundary[1]: raise ValueError(f"Offset {offset} exceeds downstream region.") else: - if abs(offset) >= self._loci[self._direction(len(self._locations))].boundary[0]: + if abs(offset) > self._loci[self._direction(len(self._locations)-1)].boundary[0]: raise ValueError(f"Offset {offset} exceeds downstream boundary.") # '' region validation, position should be within the MultiLocus and offset should not exceed intron length diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index 13280a8..236c0c9 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -76,6 +76,18 @@ def test_MultiLocus_invalid_coordinate(): multi_locus.to_position(Coord("31")) +def test_invalid_Point_initialization(): + """Test Point initialization.""" + with pytest.raises(ValueError): + Point(position=-1, offset=0, region='') + with pytest.raises(ValueError): + Point(position=0, offset=0, region='*') + with pytest.raises(ValueError): + Point(position=0, offset=0, region=None) + with pytest.raises(ValueError): + Point(position="11", offset=0, region='u') + + def test_MultiLocus(): """Forward oriented MultiLocus.""" multi_locus = MultiLocus(_locations) @@ -600,10 +612,11 @@ def test_downstream_invalid_offset(): def test_downstream_invalid_offset_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) with pytest.raises(ValueError): - multi_locus.to_coordinate(Point(position=0, offset=1, region='d')) + multi_locus.to_coordinate(Point(position=9, offset=-1, region='d')) with pytest.raises(ValueError): - multi_locus.to_coordinate(Point(position=0, offset=0, region='d')) + multi_locus.to_coordinate(Point(position=9, offset=0, region='d')) with pytest.raises(ValueError): - multi_locus.to_coordinate(Point(position=0, offset=-5, region='d')) + multi_locus.to_coordinate(Point(position=9, offset=-5, region='d')) with pytest.raises(ValueError): - multi_locus.to_coordinate(Point(position=0, offset=6, region='d')) + multi_locus.to_coordinate(Point(position=9, offset=6, region='d')) + From 8f0fdfb594509d60d445a05e2a8ff6401c15c294 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 27 Aug 2026 15:31:06 +0200 Subject: [PATCH 189/236] test_locus: Detailed error message. --- mutalyzer_crossmapper/locus.py | 2 +- tests/test_locus.py | 91 ++++++++++++++++++++++------------ 2 files changed, 61 insertions(+), 32 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 390c1a1..2c90917 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -67,7 +67,7 @@ def _validate_point(self, position: int, offset: int) -> None: :arg int offset: Offset. """ if offset != 0 and position not in (0, self._end-1): - raise ValueError(f"Position {position} is not at locus boundary.") + raise ValueError(f"Position {position} is not at a locus boundary.") if offset < 0 and position != 0: raise IndexError(f"Offset {offset} should be at a locus start.") if offset > 0 and position != self._end-1: diff --git a/tests/test_locus.py b/tests/test_locus.py index 9656a25..b70f8d8 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -6,80 +6,109 @@ def test_invalid_Locus_initialization(): """Test Locus initialization.""" - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: Locus((10, 5)) - with pytest.raises(ValueError): + assert str(e.value) == f"Locus start 10 must be smaller than locus end 5." + with pytest.raises(ValueError) as e: Locus((10, 20, 30)) - with pytest.raises(ValueError): + assert str(e.value) == f"Locus must be a tuple of two values." + with pytest.raises(ValueError) as e: Locus((10, -5)) - with pytest.raises(ValueError): + assert str(e.value) == f"Value must be non-negative." + with pytest.raises(ValueError) as e: Locus((10, 20.5)) - with pytest.raises(ValueError): - Locus((10.5, None)) - with pytest.raises(ValueError): + assert str(e.value) == f"Value must be an integer." + with pytest.raises(ValueError) as e: + Locus((10, None)) + assert str(e.value) == f"Value must be an integer." + with pytest.raises(ValueError) as e: Locus(("10", "20")) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: Locus((10, 10)) + assert str(e.value) == f"Locus start 10 must be smaller than locus end 10." #Inverted Locus initialization - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: Locus((10, 5), inverted=True) - with pytest.raises(ValueError): + assert str(e.value) == f"Locus start 10 must be smaller than locus end 5." + with pytest.raises(ValueError) as e: Locus((10, 20, 30), inverted=True) - with pytest.raises(ValueError): + assert str(e.value) == f"Locus must be a tuple of two values." + with pytest.raises(ValueError) as e: Locus((10, -5), inverted=True) - with pytest.raises(ValueError): + assert str(e.value) == f"Value must be non-negative." + with pytest.raises(ValueError) as e: Locus((10, 20.5), inverted=True) - with pytest.raises(ValueError): - Locus((10.5, None), inverted=True) - with pytest.raises(ValueError): + assert str(e.value) == f"Value must be an integer." + with pytest.raises(ValueError) as e: + Locus((10, None), inverted=True) + assert str(e.value) == f"Value must be an integer." + with pytest.raises(ValueError) as e: Locus(("10", "20"), inverted=True) - with pytest.raises(ValueError): + assert str(e.value) == f"Value must be an integer." + with pytest.raises(ValueError) as e: Locus((10, 10), inverted=True) + assert str(e.value) == f"Locus start 10 must be smaller than locus end 10." def test_invalid_Coord_initialization(): """Test Coord initialization.""" - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: Coord(-1) - with pytest.raises(ValueError): + assert str(e.value) == f"Value must be non-negative." + with pytest.raises(ValueError) as e: Coord(3.5) - with pytest.raises(ValueError): + assert str(e.value) == f"Value must be an integer." + with pytest.raises(ValueError) as e: Coord("10") - with pytest.raises(ValueError): + assert str(e.value) == f"Value must be an integer." + with pytest.raises(ValueError) as e: Coord(None) - with pytest.raises(ValueError): + assert str(e.value) == f"Value must be an integer." + with pytest.raises(ValueError) as e: Coord([10]) + assert str(e.value) == f"Value must be an integer." def test_invalid_Locus_point(): """Forward orientent Locus with invalid point.""" locus = Locus((30, 35)) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: locus.to_coordinate(Point(position=-5, offset=0)) - with pytest.raises(IndexError): + assert str(e.value) == f"Value must be non-negative." + + with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=5, offset=0)) - with pytest.raises(IndexError): + assert str(e.value) == f"Position 5 exceeds locus length." + with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=0, offset=2)) - with pytest.raises(IndexError): + assert str(e.value) == f"Offset 2 should be at a locus end." + with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=4, offset=-2)) - with pytest.raises(ValueError): + assert str(e.value) == f"Offset -2 should be at a locus start." + with pytest.raises(ValueError) as e: locus.to_coordinate(Point(position=2, offset=1)) + assert str(e.value) == f"Position 2 is not at a locus boundary." def test_invalid_Locus_inverted_point(): """Reverse orientent Locus with invalid point.""" locus = Locus((30, 35), True) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: locus.to_coordinate(Point(position=-5, offset=0)) - with pytest.raises(IndexError): + assert str(e.value) == f"Value must be non-negative." + with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=5, offset=0)) - with pytest.raises(IndexError): + assert str(e.value) == f"Position 5 exceeds locus length." + with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=0, offset=2)) - with pytest.raises(IndexError): + assert str(e.value) == f"Offset 2 should be at a locus end." + with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=4, offset=-2)) - with pytest.raises(ValueError): + assert str(e.value) == f"Offset -2 should be at a locus start." + with pytest.raises(ValueError) as e: locus.to_coordinate(Point(position=2, offset=1)) + assert str(e.value) == f"Position 2 is not at a locus boundary." def test_Locus(): From c8e07acf03fc8dc880bdb854ac251b94a7c05686 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 27 Aug 2026 16:08:50 +0200 Subject: [PATCH 190/236] Locus: Update docstring. --- mutalyzer_crossmapper/locus.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 2c90917..d859a58 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -3,7 +3,7 @@ @dataclass(slots=True) class Point: - """Point dataclass""" + """Locus point dataclass""" position: int offset: int = 0 @@ -61,7 +61,7 @@ def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: self._end = location[1] - location[0] # one-based length of the locus def _validate_point(self, position: int, offset: int) -> None: - """Validate a point model under HGVS rules. + """Validate a point according to HGVS rules. :arg int position: Position. :arg int offset: Offset. @@ -105,5 +105,5 @@ def to_coordinate(self, point: Point) -> Coord: self._validate_point(point.position, point.offset) if self._inverted: - return Coord(coordinate=self.boundary[1] - point.position - point.offset) - return Coord(coordinate=self.boundary[0] + point.position + point.offset) + return Coord(self.boundary[1] - point.position - point.offset) + return Coord(self.boundary[0] + point.position + point.offset) From 3ca68c22b1451fc52db2105cb4e73a3c064f6b32 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Thu, 27 Aug 2026 16:14:15 +0200 Subject: [PATCH 191/236] terst_locus: style according to flake8 formatting. --- tests/test_locus.py | 63 +++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/tests/test_locus.py b/tests/test_locus.py index b70f8d8..59b2be7 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -1,73 +1,74 @@ -from mutalyzer_crossmapper.locus import Locus, Point, Coord +import pytest from helper import invariant -import pytest +from mutalyzer_crossmapper.locus import Coord, Locus, Point def test_invalid_Locus_initialization(): """Test Locus initialization.""" with pytest.raises(ValueError) as e: Locus((10, 5)) - assert str(e.value) == f"Locus start 10 must be smaller than locus end 5." + assert str(e.value) == "Locus start 10 must be smaller than locus end 5." with pytest.raises(ValueError) as e: Locus((10, 20, 30)) - assert str(e.value) == f"Locus must be a tuple of two values." + assert str(e.value) == "Locus must be a tuple of two values." with pytest.raises(ValueError) as e: Locus((10, -5)) - assert str(e.value) == f"Value must be non-negative." + assert str(e.value) == "Value must be non-negative." with pytest.raises(ValueError) as e: Locus((10, 20.5)) - assert str(e.value) == f"Value must be an integer." + assert str(e.value) == "Value must be an integer." with pytest.raises(ValueError) as e: Locus((10, None)) - assert str(e.value) == f"Value must be an integer." + assert str(e.value) == "Value must be an integer." with pytest.raises(ValueError) as e: Locus(("10", "20")) + assert str(e.value) == "Value must be an integer." with pytest.raises(ValueError) as e: Locus((10, 10)) - assert str(e.value) == f"Locus start 10 must be smaller than locus end 10." + assert str(e.value) == "Locus start 10 must be smaller than locus end 10." - #Inverted Locus initialization + # Inverted Locus initialization with pytest.raises(ValueError) as e: Locus((10, 5), inverted=True) - assert str(e.value) == f"Locus start 10 must be smaller than locus end 5." + assert str(e.value) == "Locus start 10 must be smaller than locus end 5." with pytest.raises(ValueError) as e: Locus((10, 20, 30), inverted=True) - assert str(e.value) == f"Locus must be a tuple of two values." + assert str(e.value) == "Locus must be a tuple of two values." with pytest.raises(ValueError) as e: Locus((10, -5), inverted=True) - assert str(e.value) == f"Value must be non-negative." + assert str(e.value) == "Value must be non-negative." with pytest.raises(ValueError) as e: Locus((10, 20.5), inverted=True) - assert str(e.value) == f"Value must be an integer." + assert str(e.value) == "Value must be an integer." with pytest.raises(ValueError) as e: Locus((10, None), inverted=True) - assert str(e.value) == f"Value must be an integer." + assert str(e.value) == "Value must be an integer." with pytest.raises(ValueError) as e: Locus(("10", "20"), inverted=True) - assert str(e.value) == f"Value must be an integer." + assert str(e.value) == "Value must be an integer." with pytest.raises(ValueError) as e: Locus((10, 10), inverted=True) - assert str(e.value) == f"Locus start 10 must be smaller than locus end 10." + assert str(e.value) == "Locus start 10 must be smaller than locus end 10." def test_invalid_Coord_initialization(): """Test Coord initialization.""" with pytest.raises(ValueError) as e: Coord(-1) - assert str(e.value) == f"Value must be non-negative." + assert str(e.value) == "Value must be non-negative." with pytest.raises(ValueError) as e: Coord(3.5) - assert str(e.value) == f"Value must be an integer." + assert str(e.value) == "Value must be an integer." with pytest.raises(ValueError) as e: Coord("10") - assert str(e.value) == f"Value must be an integer." + assert str(e.value) == "Value must be an integer." with pytest.raises(ValueError) as e: Coord(None) - assert str(e.value) == f"Value must be an integer." + assert str(e.value) == "Value must be an integer." with pytest.raises(ValueError) as e: Coord([10]) - assert str(e.value) == f"Value must be an integer." + assert str(e.value) == "Value must be an integer." def test_invalid_Locus_point(): @@ -75,20 +76,20 @@ def test_invalid_Locus_point(): locus = Locus((30, 35)) with pytest.raises(ValueError) as e: locus.to_coordinate(Point(position=-5, offset=0)) - assert str(e.value) == f"Value must be non-negative." + assert str(e.value) == "Value must be non-negative." with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=5, offset=0)) - assert str(e.value) == f"Position 5 exceeds locus length." + assert str(e.value) == "Position 5 exceeds locus length." with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=0, offset=2)) - assert str(e.value) == f"Offset 2 should be at a locus end." + assert str(e.value) == "Offset 2 should be at a locus end." with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=4, offset=-2)) - assert str(e.value) == f"Offset -2 should be at a locus start." + assert str(e.value) == "Offset -2 should be at a locus start." with pytest.raises(ValueError) as e: locus.to_coordinate(Point(position=2, offset=1)) - assert str(e.value) == f"Position 2 is not at a locus boundary." + assert str(e.value) == "Position 2 is not at a locus boundary." def test_invalid_Locus_inverted_point(): @@ -96,19 +97,19 @@ def test_invalid_Locus_inverted_point(): locus = Locus((30, 35), True) with pytest.raises(ValueError) as e: locus.to_coordinate(Point(position=-5, offset=0)) - assert str(e.value) == f"Value must be non-negative." + assert str(e.value) == "Value must be non-negative." with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=5, offset=0)) - assert str(e.value) == f"Position 5 exceeds locus length." + assert str(e.value) == "Position 5 exceeds locus length." with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=0, offset=2)) - assert str(e.value) == f"Offset 2 should be at a locus end." + assert str(e.value) == "Offset 2 should be at a locus end." with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=4, offset=-2)) - assert str(e.value) == f"Offset -2 should be at a locus start." + assert str(e.value) == "Offset -2 should be at a locus start." with pytest.raises(ValueError) as e: locus.to_coordinate(Point(position=2, offset=1)) - assert str(e.value) == f"Position 2 is not at a locus boundary." + assert str(e.value) == "Position 2 is not at a locus boundary." def test_Locus(): From 875d3245bbe23131a72a6bf90627d92f8fdb5da9 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 28 Aug 2026 09:57:37 +0200 Subject: [PATCH 192/236] test_multi_locus: Detailed error message. --- mutalyzer_crossmapper/multi_locus.py | 37 +++-- tests/test_multi_locus.py | 231 ++++++++++++++++++--------- 2 files changed, 176 insertions(+), 92 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 229567a..34a1322 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -22,7 +22,7 @@ def __post_init__(self) -> None: def _check_in_range(value: int, length: int | None = None) -> None: """Check if the value no larger than length.""" if length is not None and value >= length: - raise ValueError(f"Value {value} must be within the bounds of the reference sequence {length}.") + raise ValueError(f"Value {value} must be within the bounds of the reference length {length}.") def _check_multi_locus(locations: list[tuple[int, int]], length: int | None = None) -> None: @@ -92,7 +92,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - raise ValueError(f"Offset {offset} exceeds upstream region.") else: if abs(offset) > self._loci[self._direction(0)].boundary[0]: - raise ValueError(f"Offset {offset} exceeds upstream boundary.") + raise ValueError(f"Offset {offset} exceeds upstream region.") # Downstream region validation, position is constant value and offset should not be negative if region == 'd': @@ -105,16 +105,17 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - raise ValueError(f"Offset {offset} exceeds downstream region.") else: if abs(offset) > self._loci[self._direction(len(self._locations)-1)].boundary[0]: - raise ValueError(f"Offset {offset} exceeds downstream boundary.") + raise ValueError(f"Offset {offset} exceeds downstream region.") # '' region validation, position should be within the MultiLocus and offset should not exceed intron length if region == '': if self._inverted: if offset < 0: if self._direction(index) == len(self._loci) - 1: - raise ValueError( - f"Offset {offset} at the last exon on the reverse complement should be in the upstream region." - ) + if position == 0: + raise ValueError( + f"Offset {offset} at the first exon on the reverse complement should be in the upstream region." + ) else: intron_length = abs( self._loci[self._direction(index-1)].boundary[0] @@ -124,9 +125,10 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - raise IndexError(f"Offset {offset} exceeds intron length.") if offset > 0: if self._direction(index) == 0: - raise ValueError( - f"Offset {offset} at the last exon on the reverse complement should be in the downstream region." - ) + if position == self._end-1: + raise ValueError( + f"Offset {offset} at the first exon on the reverse complement should be in the downstream region." + ) else: intron_length = abs( self._loci[self._direction(index)].boundary[0] @@ -139,9 +141,10 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - if not self._inverted: if offset < 0: if self._direction(index) == 0: - raise ValueError( - f"Offset {offset} at the first exon should be in the upstream region." - ) + if position == 0: + raise ValueError( + f"Offset {offset} at the first exon should be in the upstream region." + ) else: intron_length = abs( self._loci[self._direction(index)].boundary[0] @@ -151,9 +154,10 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - raise IndexError(f"Offset {offset} exceeds intron length.") if offset > 0: if self._direction(index) == len(self._loci) - 1: - raise ValueError( - f"Offset {offset} at the last exon should be in the downstream region." - ) + if position == self._end-1: + raise ValueError( + f"Offset {offset} at the last exon should be in the downstream region." + ) else: intron_length = abs( self._loci[self._direction(index+1)].boundary[0] @@ -232,8 +236,7 @@ def to_coordinate(self, point: Point) -> Coord: except ValueError as e: if "Position" in str(e): - raise ValueError(str(e).replace(str(point.position - 1), str(point.position))) from e - raise e + raise ValueError(str(e).replace(str(point.position - self._offsets[index]), str(point.position))) from e except IndexError as e: if "Position" in str(e): raise IndexError( diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index 236c0c9..da95e0b 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -1,3 +1,4 @@ +from mutalyzer_crossmapper import multi_locus from mutalyzer_crossmapper.multi_locus import _offsets, Coord, MultiLocus, Point from helper import invariant @@ -29,63 +30,86 @@ def test_offsets_adjacent_inverted(): ## Test MultiLocus model and its point model def test_invalid_MultiLocus_initialization(): """Test MultiLocus initialization.""" - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: MultiLocus(([(10, 5), (20, 25)])) - with pytest.raises(ValueError): + assert str(e.value) == "Locus start 10 must be smaller than locus end 5." + with pytest.raises(ValueError) as e: MultiLocus([(10, 20, 30), (40, 50)]) - with pytest.raises(ValueError): + assert str(e.value) == "Locus must be a tuple of two values." + with pytest.raises(ValueError) as e: MultiLocus([(10, -5), (20, 25)]) - with pytest.raises(ValueError): + assert str(e.value) == "Value must be non-negative." + with pytest.raises(ValueError) as e: MultiLocus([(10, 20.5), (30, 40)]) - with pytest.raises(ValueError): + assert str(e.value) == "Value must be an integer." + with pytest.raises(ValueError) as e: MultiLocus([(10.5, None), (20, 30)]) - with pytest.raises(ValueError): + assert str(e.value) == "Value must be an integer." + with pytest.raises(ValueError) as e: MultiLocus([("10", "20"), (30, 40)]) - with pytest.raises(ValueError): + assert str(e.value) == "Value must be an integer." + with pytest.raises(ValueError) as e: MultiLocus([(10, 20), (15, 25)]) - with pytest.raises(ValueError): + assert str(e.value) == "Locus (15, 25) and locus (10, 20) are overlapping." + with pytest.raises(ValueError) as e: MultiLocus([(10, 12), (15, 25)], 25) + assert str(e.value) == "Value 25 must be within the bounds of the reference length 25." # Inverted MultiLocus initialization - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: MultiLocus(([(10, 5), (20, 25)]), inverted=True) - with pytest.raises(ValueError): + assert str(e.value) == "Locus start 10 must be smaller than locus end 5." + with pytest.raises(ValueError) as e: MultiLocus([(10, 20, 30), (40, 50)], inverted=True) - with pytest.raises(ValueError): + assert str(e.value) == "Locus must be a tuple of two values." + with pytest.raises(ValueError) as e: MultiLocus([(10, -5), (20, 25)], inverted=True) - with pytest.raises(ValueError): + assert str(e.value) == "Value must be non-negative." + with pytest.raises(ValueError) as e: MultiLocus([(10, 20.5), (30, 40)], inverted=True) - with pytest.raises(ValueError): + assert str(e.value) == "Value must be an integer." + with pytest.raises(ValueError) as e: MultiLocus([(10.5, None), (20, 30)], inverted=True) - with pytest.raises(ValueError): + assert str(e.value) == "Value must be an integer." + with pytest.raises(ValueError) as e: MultiLocus([("10", "20"), (30, 40)], inverted=True) - with pytest.raises(ValueError): + assert str(e.value) == "Value must be an integer." + with pytest.raises(ValueError) as e: MultiLocus([(10, 20), (15, 25)], 25, inverted=True) - with pytest.raises(ValueError): + assert str(e.value) == "Locus (15, 25) and locus (10, 20) are overlapping." + with pytest.raises(ValueError) as e: MultiLocus([(10, 12), (15, 25)], 25, inverted=True) + assert str(e.value) == "Value 25 must be within the bounds of the reference length 25." def test_MultiLocus_invalid_coordinate(): """Forward orientent MultiLocus with invalid coordinate.""" multi_locus = MultiLocus([(30, 35), (40, 45)]) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_position(Coord(-1)) - with pytest.raises(ValueError): + assert str(e.value) == "Value must be non-negative." + with pytest.raises(ValueError) as e: multi_locus.to_position(Coord(46.7)) - with pytest.raises(ValueError): + assert str(e.value) == "Value must be an integer." + with pytest.raises(ValueError) as e: multi_locus.to_position(Coord("31")) + assert str(e.value) == "Value must be an integer." def test_invalid_Point_initialization(): """Test Point initialization.""" - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: Point(position=-1, offset=0, region='') - with pytest.raises(ValueError): + assert str(e.value) == "Value must be non-negative." + with pytest.raises(ValueError) as e: Point(position=0, offset=0, region='*') - with pytest.raises(ValueError): + assert str(e.value) == "Region * is not valid. Must be '', 'u', or 'd'." + with pytest.raises(ValueError) as e: Point(position=0, offset=0, region=None) - with pytest.raises(ValueError): + assert str(e.value) == "Region None is not valid. Must be '', 'u', or 'd'." + with pytest.raises(ValueError) as e: Point(position="11", offset=0, region='u') + assert str(e.value) == "Value must be an integer." def test_MultiLocus(): @@ -255,9 +279,9 @@ def test_MultiLocus_with_length(): Point(position=21, offset=2, region='d'), ) # Boundary between the last base and beyond the last base. - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_position(Coord(74)) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=21, offset=3, region='d')) @@ -285,9 +309,9 @@ def test_MultiLocus_inverted_with_length(): multi_locus.to_coordinate, Point(position=0, offset=-2, region='u'), ) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_position(Coord(74)) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=-3, region='u')) @@ -482,141 +506,198 @@ def test_one_base_exon_inverted(): def test_upstream_invalid_position(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=1, offset=-1, region='u')) - with pytest.raises(ValueError): + assert str(e.value) == "Position 1 is not at upstream boundary." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=-1, offset=-1, region='u')) - with pytest.raises(ValueError): + assert str(e.value) == "Value must be non-negative." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=20, offset=-1, region='u')) + assert str(e.value) == "Position 20 is not at upstream boundary." def test_upstream_invalid_position_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=1, offset=-1, region='u')) - with pytest.raises(ValueError): + assert str(e.value) == "Position 1 is not at upstream boundary." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=-1, offset=-1, region='u')) - with pytest.raises(ValueError): + assert str(e.value) == "Value must be non-negative." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=20, offset=-1, region='u')) + assert str(e.value) == "Position 20 is not at upstream boundary." def test_upstream_invalid_offset(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=1, region='u')) - with pytest.raises(ValueError): + assert str(e.value) == "Offset 1 at upstream boundary should be negative." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=-6, region='u')) - with pytest.raises(ValueError): + assert str(e.value) == "Offset -6 exceeds upstream region." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=0, region='u')) + assert str(e.value) == "Offset 0 at upstream boundary should be negative." def test_upstream_invalid_offset_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=1, region='u')) - with pytest.raises(ValueError): + assert str(e.value) == "Offset 1 at upstream boundary should be negative." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=-6, region='u')) - with pytest.raises(ValueError): + assert str(e.value) == "Offset -6 exceeds upstream region." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=0, region='u')) + assert str(e.value) == "Offset 0 at upstream boundary should be negative." def test_transcribed_invalid_position(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=-1, offset=0, region='')) - with pytest.raises(ValueError): + assert str(e.value) == "Value must be non-negative." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=7, offset=1, region='')) - with pytest.raises(IndexError): + assert str(e.value) == "Position 7 is not at a locus boundary." + with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=20, offset=0, region='')) + assert str(e.value) == "Position 20 exceeds multi locus length." def test_transcribed_invalid_position_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=-1, offset=0, region='')) - with pytest.raises(ValueError): + assert str(e.value) == "Value must be non-negative." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=7, offset=-1, region='')) - with pytest.raises(IndexError): + assert str(e.value) == "Position 7 is not at a locus boundary." + with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=20, offset=0, region='')) + assert str(e.value) == "Position 20 exceeds multi locus length." def test_transcribed_invalid_offset(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=-5, region='')) - with pytest.raises(IndexError): + assert str(e.value) == "Offset -5 at the first exon should be in the upstream region." + with pytest.raises(IndexError) as e: + multi_locus.to_coordinate(Point(position=0, offset=1, region='')) + assert str(e.value) == "Offset 1 should be at a locus end." + with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=0, offset=10, region='')) - with pytest.raises(IndexError): + assert str(e.value) == "Offset 10 exceeds intron length." + with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=4, offset=6, region='')) - with pytest.raises(ValueError): + assert str(e.value) == "Offset 6 exceeds intron length." + with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=4, offset=-1, region='')) - with pytest.raises(IndexError): + assert str(e.value) == "Offset -1 should be at a locus start." + with pytest.raises(IndexError) as e: + multi_locus.to_coordinate(Point(position=5, offset=2, region='')) + assert str(e.value) == "Offset 2 should be at a locus end." + with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=5, offset=-6, region='')) - with pytest.raises(ValueError): - multi_locus.to_coordinate(Point(position=5, offset=1, region='')) - + assert str(e.value) == "Offset -6 exceeds intron length." + with pytest.raises(ValueError) as e: + multi_locus.to_coordinate(Point(position=9, offset=1, region='')) + assert str(e.value) == "Offset 1 at the last exon should be in the downstream region." def test_transcribed_invalid_offset_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=5, region='')) - with pytest.raises(IndexError): + assert str(e.value) == "Offset 5 at the first exon on the reverse complement should be in the downstream region." + with pytest.raises(IndexError) as e: + multi_locus.to_coordinate(Point(position=9, offset=-1, region='')) + assert str(e.value) == "Offset -1 should be at a locus start." + with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=9, offset=-10, region='')) - with pytest.raises(IndexError): + assert str(e.value) == "Offset -10 exceeds intron length." + with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=5, offset=-6, region='')) - with pytest.raises(ValueError): + assert str(e.value) == "Offset -6 exceeds intron length." + with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=5, offset=1, region='')) - with pytest.raises(IndexError): + assert str(e.value) == "Offset 1 should be at a locus end." + with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=4, offset=6, region='')) - with pytest.raises(ValueError): + assert str(e.value) == "Offset 6 exceeds intron length." + with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=4, offset=-1, region='')) + assert str(e.value) == "Offset -1 should be at a locus start." + with pytest.raises(ValueError) as e: + multi_locus.to_coordinate(Point(position=0, offset=-5, region='')) + assert str(e.value) == "Offset -5 at the first exon on the reverse complement should be in the upstream region." def test_downstream_invalid_position(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=1, region='d')) - with pytest.raises(ValueError): + assert str(e.value) == "Position 0 is not at downstream boundary." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=-1, offset=1, region='d')) - with pytest.raises(ValueError): + assert str(e.value) == "Value must be non-negative." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=11, offset=1, region='d')) - with pytest.raises(ValueError): + assert str(e.value) == "Position 11 is not at downstream boundary." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=100, offset=1, region='d')) + assert str(e.value) == "Position 100 is not at downstream boundary." def test_downstream_invalid_position_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=1, region='d')) - with pytest.raises(ValueError): + assert str(e.value) == "Position 0 is not at downstream boundary." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=-1, offset=1, region='d')) - with pytest.raises(ValueError): + assert str(e.value) == "Value must be non-negative." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=11, offset=1, region='d')) - with pytest.raises(ValueError): + assert str(e.value) == "Position 11 is not at downstream boundary." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=100, offset=1, region='d')) + assert str(e.value) == "Position 100 is not at downstream boundary." def test_downstream_invalid_offset(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=-1, region='d')) - with pytest.raises(ValueError): + assert str(e.value) == "Offset -1 at downstream boundary should be positive." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=0, region='d')) - with pytest.raises(ValueError): + assert str(e.value) == "Offset 0 at downstream boundary should be positive." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=-5, region='d')) - with pytest.raises(ValueError): + assert str(e.value) == "Offset -5 at downstream boundary should be positive." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=6, region='d')) + assert str(e.value) == "Offset 6 exceeds downstream region." def test_downstream_invalid_offset_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=-1, region='d')) - with pytest.raises(ValueError): + assert str(e.value) == "Offset -1 at downstream boundary should be positive." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=0, region='d')) - with pytest.raises(ValueError): + assert str(e.value) == "Offset 0 at downstream boundary should be positive." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=-5, region='d')) - with pytest.raises(ValueError): + assert str(e.value) == "Offset -5 at downstream boundary should be positive." + with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=6, region='d')) + assert str(e.value) == "Offset 6 exceeds downstream region." From 92d9c562c88b0da700bbaef4ed74f46cba160415 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Fri, 28 Aug 2026 09:58:22 +0200 Subject: [PATCH 193/236] test_crossmapper: Detailed error message. --- mutalyzer_crossmapper/crossmapper.py | 23 +++-- tests/test_crossmapper.py | 149 ++++++++++++++++++++++++++- 2 files changed, 160 insertions(+), 12 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index cc1e8ab..8089d9f 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -12,7 +12,7 @@ class GenomicPoint: def __post_init__(self) -> None: _check_int(self.position) if self.position <= 0: - raise ValueError("Genomic position must be a positive integer.") + raise ValueError(f"Position {self.position} must be a positive integer.") def __str__(self) -> str: return f'{self.position}' @@ -46,7 +46,7 @@ class NonCodingPoint(GenomicPoint): offset: int = 0 region: str = '' - allowed_regions = {'', 'u', 'd'} + allowed_regions = ['', 'u', 'd'] def __post_init__(self) -> None: # Python version 3.11 and 3.10: cannot use super() due to conflicts with slots=True @@ -54,7 +54,7 @@ def __post_init__(self) -> None: _check_int(self.offset) if self.region not in self.allowed_regions: - raise ValueError(f'Region must be a string in {self.allowed_regions}') + raise ValueError(f"Region must be a string in {self.allowed_regions}.") def __str__(self) -> str: if self.offset == 0: @@ -119,7 +119,7 @@ def noncoding_to_coordinate(self, point: NonCodingPoint) -> Coord: @dataclass(slots=True) class CodingPoint(NonCodingPoint): """Coding dataclass.""" - allowed_regions = {'', 'u', 'd', '-', '*'} + allowed_regions = ['', 'u', 'd', '-', '*'] @dataclass(slots=True) @@ -305,9 +305,18 @@ def _coding_to_coordinate(self, point: CodingPoint) -> Coord: elif region == '*': position = self._coding[1] + position - 1 - return self._noncoding.to_coordinate( - Point(position=position, region='', offset=point.offset) - ) + try: + return self._noncoding.to_coordinate( + Point(position=position, region='', offset=point.offset) + ) + except ValueError as e: + if "Position" in str(e): + raise ValueError(str(e).replace(str(position), str(point.position))) + raise e + except IndexError as e: + if "Position" in str(e): + raise IndexError(str(e).replace(str(position), str(point.position))) + raise e def coding_to_coordinate(self, point: CodingPoint) -> Coord: """Convert a coding position (c./r.) to a coordinate. diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 6ef3f51..54fb811 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -11,10 +11,13 @@ def test_GenomicPoint_invalid_initialization(): """GenomicPoint cannot be initialized with invalid position.""" with pytest.raises(ValueError) as e: GenomicPoint(position=0) + assert str(e.value) == "Position 0 must be a positive integer." with pytest.raises(ValueError) as e: GenomicPoint(position=-1) + assert str(e.value) == "Position -1 must be a positive integer." with pytest.raises(ValueError) as e: GenomicPoint(position=[101]) + assert str(e.value) == "Value must be an integer." def test_Genomic(): @@ -40,8 +43,10 @@ def test_Genomic_invalid_with_length(): crossmap = Genomic() with pytest.raises(ValueError) as e: crossmap.coordinate_to_genomic(Coord(-1), 99) + assert str(e.value) == "Value must be non-negative." with pytest.raises(ValueError) as e: crossmap.coordinate_to_genomic(Coord(99), 99) + assert str(e.value) == "Value 99 must be within the bounds of the reference length 99." def test_Genomic_with_length(): @@ -66,51 +71,72 @@ def test_NonCodingPoint_invalid_initialization(): """Raise error with invalid initialization.""" with pytest.raises(ValueError) as e: NonCodingPoint(position=0, offset=0, region='u') + assert str(e.value) == "Position 0 must be a positive integer." with pytest.raises(ValueError) as e: NonCodingPoint(position=0, offset=0, region='d') + assert str(e.value) == "Position 0 must be a positive integer." with pytest.raises(ValueError) as e: NonCodingPoint(position=0, offset=0, region='') + assert str(e.value) == "Position 0 must be a positive integer." with pytest.raises(ValueError) as e: NonCodingPoint(position=0, offset=0, region='*') + assert str(e.value) == "Position 0 must be a positive integer." with pytest.raises(ValueError) as e: NonCodingPoint(position=-1, offset=0, region='') + assert str(e.value) == "Position -1 must be a positive integer." with pytest.raises(ValueError) as e: NonCodingPoint(position=1, offset=None, region='u') + assert str(e.value) == "Value must be an integer." + with pytest.raises(ValueError) as e: + NonCodingPoint(position=1, offset=1, region='-') + assert str(e.value) == "Region must be a string in ['', 'u', 'd']." def test_NonCoding_invalid(): """Raise ValueError if noncoding is invalid.""" with pytest.raises(ValueError) as e: NonCoding([()]) + assert str(e.value) == "Locus must be a tuple of two values." with pytest.raises(ValueError) as e: NonCoding([(10)]) + assert str(e.value) == "Locus must be a tuple of two values." with pytest.raises(ValueError) as e: NonCoding([(10, 20), (15, 25)]) + assert str(e.value) == "Locus (15, 25) and locus (10, 20) are overlapping." with pytest.raises(ValueError) as e: NonCoding([(None, 20), (30, None)]) + assert str(e.value) == "Value must be an integer." with pytest.raises(ValueError) as e: NonCoding(_exons, length=70) + assert str(e.value) == "Value 72 must be within the bounds of the reference length 70." # Reverse orientation with pytest.raises(ValueError) as e: NonCoding([()], inverted=True) + assert str(e.value) == "Locus must be a tuple of two values." with pytest.raises(ValueError) as e: NonCoding([(10)], inverted=True) + assert str(e.value) == "Locus must be a tuple of two values." with pytest.raises(ValueError) as e: NonCoding([(10, 20), (15, 25)], inverted=True) + assert str(e.value) == "Locus (15, 25) and locus (10, 20) are overlapping." with pytest.raises(ValueError) as e: NonCoding([(None, 20), (30, None)], inverted=True) + assert str(e.value) == "Value must be an integer." with pytest.raises(ValueError) as e: NonCoding(_exons, length=70, inverted=True) + assert str(e.value) == "Value 72 must be within the bounds of the reference length 70." def test_NonCoding_invalid_with_length(): """Raise ValueError if coordinate is out of bounds.""" with pytest.raises(ValueError) as e: NonCoding(_exons, length=70) + assert str(e.value) == "Value 72 must be within the bounds of the reference length 70." # Reverse orientation with pytest.raises(ValueError) as e: NonCoding(_exons, length=70, inverted=True) + assert str(e.value) == "Value 72 must be within the bounds of the reference length 70." def test_NonCoding(): @@ -199,8 +225,10 @@ def test_NonCoding_with_length(): ) with pytest.raises(ValueError) as e: crossmap.coordinate_to_noncoding(Coord(75)) + assert str(e.value) == "Value 75 must be within the bounds of the reference length 75." with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=4, region='d')) + assert str(e.value) == "Offset 4 exceeds downstream region." def test_NonCoding_inverted(): @@ -243,8 +271,10 @@ def test_NonCoding_inverted_with_length(): # Boundary between upstream and sequence end. with pytest.raises(ValueError) as e: crossmap.coordinate_to_noncoding(Coord(75)) + assert str(e.value) == "Value 75 must be within the bounds of the reference length 75." with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-4, region='u')) + assert str(e.value) == "Offset -4 exceeds upstream region." invariant( crossmap.coordinate_to_noncoding, Coord(74), @@ -286,14 +316,19 @@ def test_NonCoding_invalid_position(): crossmap = NonCoding(_exons, length=75) with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=0, offset=1, region='u')) + assert str(e.value) == "Position 0 must be a positive integer." with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=2, offset=1, region='u')) - with pytest.raises(IndexError): + assert str(e.value) == "Position 2 is not at upstream boundary." + with pytest.raises(IndexError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=23, offset=0, region='')) + assert str(e.value) == "Position 23 exceeds multi locus length." with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=21, offset=1, region='d')) + assert str(e.value) == "Position 21 is not at downstream boundary." with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=30, offset=-1, region='d')) + assert str(e.value) == "Position 30 is not at downstream boundary." def test_NonCoding_invalid_position_inverted(): @@ -301,14 +336,19 @@ def test_NonCoding_invalid_position_inverted(): crossmap = NonCoding(_exons, length=75, inverted=True) with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=0, offset=1, region='u')) + assert str(e.value) == "Position 0 must be a positive integer." with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=2, offset=1, region='u')) - with pytest.raises(IndexError): + assert str(e.value) == "Position 2 is not at upstream boundary." + with pytest.raises(IndexError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=23, offset=0, region='')) + assert str(e.value) == "Position 23 exceeds multi locus length." with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=21, offset=1, region='d')) + assert str(e.value) == "Position 21 is not at downstream boundary." with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=30, offset=-1, region='d')) + assert str(e.value) == "Position 30 is not at downstream boundary." def test_NonCoding_invalid_offset(): @@ -391,16 +431,22 @@ def test_CodingPoint_invalid_initialization(): """Raise error with invalid initialization.""" with pytest.raises(ValueError) as e: CodingPoint(position=0, offset=0, region='-') + assert str(e.value) == "Position 0 must be a positive integer." with pytest.raises(ValueError) as e: CodingPoint(position=0, offset=0, region='*') + assert str(e.value) == "Position 0 must be a positive integer." with pytest.raises(ValueError) as e: CodingPoint(position=0, offset=0, region='') + assert str(e.value) == "Position 0 must be a positive integer." with pytest.raises(ValueError) as e: CodingPoint(position=-1, offset=0, region='') + assert str(e.value) == "Position -1 must be a positive integer." with pytest.raises(ValueError) as e: CodingPoint(position=1, offset=None, region='') + assert str(e.value) == "Value must be an integer." with pytest.raises(ValueError) as e: CodingPoint(position=2, offset=1, region='upstream') + assert str(e.value) == "Region must be a string in ['', 'u', 'd', '-', '*']." def test_Coding_invalid(): @@ -408,35 +454,47 @@ def test_Coding_invalid(): with pytest.raises(ValueError) as e: Coding([(20, 20)], (20, 20)) + assert str(e.value) == "Locus start 20 must be smaller than locus end 20." with pytest.raises(ValueError) as e: Coding([(10, 20)], (9,15)) + assert str(e.value) == "Coordinate 9 of CDS (9, 15) is not within any exon." with pytest.raises(ValueError) as e: Coding([(10, 20)], (10,21)) + assert str(e.value) == "Coordinate 21 of CDS (10, 21) is not within any exon." with pytest.raises(ValueError) as e: Coding([(10, 20)], (15, 10)) + assert str(e.value) == "Locus start 15 must be smaller than locus end 10." with pytest.raises(ValueError) as e: Coding([], None) + assert str(e.value) == "Locations must be a non-empty list of tuples." # Reverse orientation with pytest.raises(ValueError) as e: Coding([(20, 20)], (20, 20), inverted=True) + assert str(e.value) == "Locus start 20 must be smaller than locus end 20." with pytest.raises(ValueError) as e: Coding([(10, 20)], (9,15), inverted=True) + assert str(e.value) == "Coordinate 9 of CDS (9, 15) is not within any exon." with pytest.raises(ValueError) as e: Coding([(10, 20)], (10,21), inverted=True) + assert str(e.value) == "Coordinate 21 of CDS (10, 21) is not within any exon." with pytest.raises(ValueError) as e: Coding([(10, 20)], (15, 10), inverted=True) + assert str(e.value) == "Locus start 15 must be smaller than locus end 10." with pytest.raises(ValueError) as e: Coding([], None, inverted=True) + assert str(e.value) == "Locations must be a non-empty list of tuples." def test_Coding_invalid_with_length(): """Raise ValueError if coordinate is out of bounds.""" with pytest.raises(ValueError) as e: Coding(_exons, _cds, length=70) + assert str(e.value) == "Value 72 must be within the bounds of the reference length 70." # Reverse orientation with pytest.raises(ValueError) as e: Coding(_exons, _cds, length=70, inverted=True) + assert str(e.value) == "Value 72 must be within the bounds of the reference length 70." def test_Coding(): @@ -640,8 +698,10 @@ def test_Coding_inverted_with_length(): # Boundary between upstream and sequence end. with pytest.raises(ValueError) as e: crossmap.coordinate_to_coding(Coord(75)) + assert str(e.value) == "Value 75 must be within the bounds of the reference length 75." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=5, offset=-4, region='u')) + assert str(e.value) == "Offset -4 exceeds upstream region." invariant( crossmap.coordinate_to_coding, Coord(74), @@ -1214,32 +1274,41 @@ def test_Coding_inverted_no_utr_degenerate_return(): assert crossmap.coordinate_to_coding(Coord(11), True) == CodingPoint(position=1, offset=0, region='-') assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=1, offset=0, region='*') -_exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] -_cds = (32, 43) + def test_Coding_invalid_position(): """Raise error if position in coding point is invalid.""" crossmap = Coding(_exons, _cds) with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=0, offset=1, region='u')) + assert str(e.value) == "Position 0 must be a positive integer." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=12, offset=-1, region='u')) + assert str(e.value) == "Position 12 is not in upstream boundary." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=13, offset=1, region='-')) + assert str(e.value) == "Position 13 exceeds - region." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=-1, offset=0, region='-')) + assert str(e.value) == "Position -1 must be a positive integer." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=0, offset=0, region='')) + assert str(e.value) == "Position 0 must be a positive integer." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=7, offset=0, region='')) + assert str(e.value) == "Position 7 exceeds coding region." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=6, offset=1, region='*')) + assert str(e.value) == "Position 6 exceeds * region." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=None, offset=0, region='*')) + assert str(e.value) == "Value must be an integer." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=6, offset=0, region='d')) + assert str(e.value) == "Position 6 is not in downstream boundary." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=1000, offset=2, region='d')) + assert str(e.value) == "Position 1000 is not in downstream boundary." def test_Coding_inverted_invalid_position_inverted(): @@ -1248,24 +1317,94 @@ def test_Coding_inverted_invalid_position_inverted(): with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=0, offset=1, region='u')) + assert str(e.value) == "Position 0 must be a positive integer." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=12, offset=-1, region='u')) + assert str(e.value) == "Position 12 is not in upstream boundary." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=13, offset=1, region='-')) + assert str(e.value) == "Position 13 exceeds - region." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=-1, offset=0, region='-')) + assert str(e.value) == "Position -1 must be a positive integer." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=0, offset=0, region='')) + assert str(e.value) == "Position 0 must be a positive integer." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=7, offset=0, region='')) + assert str(e.value) == "Position 7 exceeds coding region." with pytest.raises(ValueError) as e: - crossmap.coding_to_coordinate(CodingPoint(position=6, offset=1, region='*')) + crossmap.coding_to_coordinate(CodingPoint(position=13, offset=1, region='*')) + assert str(e.value) == "Position 13 exceeds * region." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=None, offset=0, region='*')) + assert str(e.value) == "Value must be an integer." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=6, offset=0, region='d')) + assert str(e.value) == "Position 6 is not in downstream boundary." with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=1000, offset=2, region='d')) + assert str(e.value) == "Position 1000 is not in downstream boundary." + + +def test_Coding_invalid_offset(): + """Raise error if offset in coding point is invalid.""" + crossmap = Coding(_exons, _cds, length=75) + + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-6, region='u')) + assert str(e.value) == "Offset -6 exceeds upstream region." + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=1, offset=1, region='-')) + assert str(e.value) == "Position 1 is not at a locus boundary." + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-1, region='-')) + assert str(e.value) == "Position 1 is not at a locus boundary." + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-1, region='')) + assert str(e.value) == "Position 1 is not at a locus boundary." + with pytest.raises(IndexError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=3, offset=6, region='')) + assert str(e.value) == "Offset 6 exceeds intron length." + with pytest.raises(IndexError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=4, offset=12, region='*')) + assert str(e.value) == "Offset 12 should be at a locus end." + with pytest.raises(IndexError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=4, offset=-50, region='*')) + assert str(e.value) == "Offset -50 exceeds intron length." + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=5, offset=10, region='d')) + assert str(e.value) == "Offset 10 exceeds downstream region." + + +def test_Coding_invalid_offset_inverted(): + """Raise error if offset in coding point is invalid.""" + crossmap = Coding(_exons, _cds, length=75, inverted=True) + + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-6, region='u')) + assert str(e.value) == "Offset -6 exceeds upstream region." + with pytest.raises(IndexError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=1, offset=1, region='-')) + assert str(e.value) == "Offset 1 should be at a locus end." + with pytest.raises(IndexError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=2, offset=-1, region='-')) + assert str(e.value) == "Offset -1 should be at a locus start." + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-1, region='')) + assert str(e.value) == "Position 1 is not at a locus boundary." + with pytest.raises(IndexError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=3, offset=6, region='')) + assert str(e.value) == "Offset 6 exceeds intron length." + with pytest.raises(IndexError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=4, offset=12, region='*')) + assert str(e.value) == "Offset 12 exceeds intron length." + with pytest.raises(IndexError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=4, offset=-50, region='*')) + assert str(e.value) == "Offset -50 exceeds intron length." + with pytest.raises(ValueError) as e: + crossmap.coding_to_coordinate(CodingPoint(position=11, offset=10, region='d')) + assert str(e.value) == "Offset 10 exceeds downstream region." def test_Coding_protein(): From f67309958252a6e4897e63ec43582d8cb59ef8fa Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 31 Aug 2026 11:10:13 +0200 Subject: [PATCH 194/236] Fix typings and use -offset for negative offset instead of absolute value. --- mutalyzer_crossmapper/crossmapper.py | 2 +- mutalyzer_crossmapper/multi_locus.py | 17 +++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 8089d9f..b0dfe35 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -189,7 +189,7 @@ def _check_cds(self, cds: tuple[int, int], locations: list[tuple[int, int]], len if coord < locations[index][0] or coord > locations[index][1]: raise ValueError(f"Coordinate {coord} of CDS {cds} is not within any exon.") - def _validate_point(self, position, region) -> None: + def _validate_point(self, position: int, region: str) -> None: """Validate a coding point model under HGVS rules. :arg CodingPoint point: Coding point model. diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 34a1322..650aa17 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -88,10 +88,10 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - if offset >= 0: raise ValueError(f"Offset {offset} at upstream boundary should be negative.") if self._inverted: - if self._length is not None and abs(offset) >= self._length - self._loci[self._direction(0)].boundary[1]: + if self._length is not None and -offset >= self._length - self._loci[self._direction(0)].boundary[1]: raise ValueError(f"Offset {offset} exceeds upstream region.") else: - if abs(offset) > self._loci[self._direction(0)].boundary[0]: + if -offset > self._loci[self._direction(0)].boundary[0]: raise ValueError(f"Offset {offset} exceeds upstream region.") # Downstream region validation, position is constant value and offset should not be negative @@ -101,10 +101,10 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - if offset <= 0: raise ValueError(f"Offset {offset} at downstream boundary should be positive.") if not self._inverted: - if self._length is not None and abs(offset) >= self._length - self._loci[self._direction(-1)].boundary[1]: + if self._length is not None and offset >= self._length - self._loci[self._direction(-1)].boundary[1]: raise ValueError(f"Offset {offset} exceeds downstream region.") else: - if abs(offset) > self._loci[self._direction(len(self._locations)-1)].boundary[0]: + if offset > self._loci[self._direction(len(self._locations)-1)].boundary[0]: raise ValueError(f"Offset {offset} exceeds downstream region.") # '' region validation, position should be within the MultiLocus and offset should not exceed intron length @@ -117,10 +117,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - f"Offset {offset} at the first exon on the reverse complement should be in the upstream region." ) else: - intron_length = abs( - self._loci[self._direction(index-1)].boundary[0] - - self._loci[self._direction(index)].boundary[1] - ) + intron_length = abs(self._loci[self._direction(index-1)].boundary[0] - self._loci[self._direction(index)].boundary[1]) if abs(offset) >= intron_length: raise IndexError(f"Offset {offset} exceeds intron length.") if offset > 0: @@ -134,7 +131,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index+1)].boundary[1] ) - if abs(offset) >= intron_length: + if abs(offset) >= self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index+1)].boundary[1]: raise IndexError(f"Offset {offset} exceeds intron length.") @@ -242,5 +239,5 @@ def to_coordinate(self, point: Point) -> Coord: raise IndexError( f"Position {point.position} exceeds multi locus length." ) from e - raise e + raise From 3aa854e60b89ceb9008b2df73badcb393ee94951 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 31 Aug 2026 11:14:30 +0200 Subject: [PATCH 195/236] Fix typing. --- mutalyzer_crossmapper/multi_locus.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 650aa17..154d9a6 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -234,6 +234,7 @@ def to_coordinate(self, point: Point) -> Coord: except ValueError as e: if "Position" in str(e): raise ValueError(str(e).replace(str(point.position - self._offsets[index]), str(point.position))) from e + raise except IndexError as e: if "Position" in str(e): raise IndexError( From 26680c5bccb17b5e15c28f14525e5b03f83fe4c2 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 31 Aug 2026 11:40:58 +0200 Subject: [PATCH 196/236] Multi_locus:Discard of absolute when validate point. --- mutalyzer_crossmapper/multi_locus.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 154d9a6..41974bb 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -117,8 +117,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - f"Offset {offset} at the first exon on the reverse complement should be in the upstream region." ) else: - intron_length = abs(self._loci[self._direction(index-1)].boundary[0] - self._loci[self._direction(index)].boundary[1]) - if abs(offset) >= intron_length: + if -offset >= self._loci[self._direction(index-1)].boundary[0] - self._loci[self._direction(index)].boundary[1]: raise IndexError(f"Offset {offset} exceeds intron length.") if offset > 0: if self._direction(index) == 0: @@ -127,11 +126,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - f"Offset {offset} at the first exon on the reverse complement should be in the downstream region." ) else: - intron_length = abs( - self._loci[self._direction(index)].boundary[0] - - self._loci[self._direction(index+1)].boundary[1] - ) - if abs(offset) >= self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index+1)].boundary[1]: + if offset >= self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index+1)].boundary[1]: raise IndexError(f"Offset {offset} exceeds intron length.") @@ -143,11 +138,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - f"Offset {offset} at the first exon should be in the upstream region." ) else: - intron_length = abs( - self._loci[self._direction(index)].boundary[0] - - self._loci[self._direction(index-1)].boundary[1] - ) - if abs(offset) >= intron_length: + if -offset >= self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index-1)].boundary[1]: raise IndexError(f"Offset {offset} exceeds intron length.") if offset > 0: if self._direction(index) == len(self._loci) - 1: @@ -156,11 +147,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - f"Offset {offset} at the last exon should be in the downstream region." ) else: - intron_length = abs( - self._loci[self._direction(index+1)].boundary[0] - - self._loci[self._direction(index)].boundary[1] - ) - if abs(offset) >= intron_length: + if offset >= self._loci[self._direction(index+1)].boundary[0] - self._loci[self._direction(index)].boundary[1]: raise IndexError(f"Offset {offset} exceeds intron length.") From 4ce97725fd7444b30d348f021ec9dd5c762b9912 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 31 Aug 2026 12:17:06 +0200 Subject: [PATCH 197/236] Discard absolute in crossmapper module and add test for protein dataclass invalid intialization. --- mutalyzer_crossmapper/crossmapper.py | 10 +++++----- tests/test_crossmapper.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index b0dfe35..217233e 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -131,7 +131,7 @@ def __post_init__(self) -> None: CodingPoint.__post_init__(self) if not isinstance(self.position_in_codon, int) or self.position_in_codon not in (1, 2, 3): - raise ValueError('Position_in_codon must be 1, 2, or 3') + raise ValueError('Position_in_codon must be 1, 2, or 3.') def __str__(self) -> str: if self.offset == 0 and self.region == '': @@ -258,12 +258,12 @@ def coordinate_to_coding(self, coord: Coord, degenerate: bool = False) -> Coding if not degenerate: return point - offset = abs(point.offset) + offset = point.offset if point.region == 'u': if self._coding[0] == 0: - position = offset + position = -offset else: - position = point.position + offset + position = point.position - offset return CodingPoint(position=position, offset=0, region='-') if point.region == 'd': if self._exons[1] == self._coding[1]: @@ -350,7 +350,7 @@ def coordinate_to_protein(self, coord: Coord) -> ProteinPoint: position = point.position if point.region in ('-', 'u'): return ProteinPoint( - position=abs(-position // 3), + position=-(-position // 3), position_in_codon=-position % 3 + 1, region=point.region, offset=point.offset diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 54fb811..2b4adb5 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -1407,6 +1407,19 @@ def test_Coding_invalid_offset_inverted(): assert str(e.value) == "Offset 10 exceeds downstream region." +def test_Coding_protein_point_invalid_intialization(): + """Raise error if protein point is initialized with invalid values.""" + with pytest.raises(ValueError) as e: + ProteinPoint(position=0, offset=0, region='u', position_in_codon=1) + assert str(e.value) == "Position 0 must be a positive integer." + with pytest.raises(ValueError) as e: + ProteinPoint(position=1, offset=0, region='', position_in_codon=4) + assert str(e.value) == "Position_in_codon must be 1, 2, or 3." + with pytest.raises(ValueError) as e: + ProteinPoint(position=1, offset=0, region='', position_in_codon=0) + assert str(e.value) == "Position_in_codon must be 1, 2, or 3." + + def test_Coding_protein(): """Protein positions.""" crossmap = Coding(_exons, _cds) From 0778fea0ffc25eb50f0dc02c95d494a4ccc16a40 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 31 Aug 2026 12:18:03 +0200 Subject: [PATCH 198/236] Discard absolute in crossmapper module and add test for protein dataclass invalid intialization. --- tests/test_crossmapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 2b4adb5..ced10f2 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -1407,7 +1407,7 @@ def test_Coding_invalid_offset_inverted(): assert str(e.value) == "Offset 10 exceeds downstream region." -def test_Coding_protein_point_invalid_intialization(): +def test_Coding_protein_point_invalid_initialization(): """Raise error if protein point is initialized with invalid values.""" with pytest.raises(ValueError) as e: ProteinPoint(position=0, offset=0, region='u', position_in_codon=1) From 127b46b1c465a779883385ad35afcdbadc235a1b Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 31 Aug 2026 14:58:44 +0200 Subject: [PATCH 199/236] Use '' for string. --- mutalyzer_crossmapper/__init__.py | 2 +- mutalyzer_crossmapper/crossmapper.py | 28 ++-- mutalyzer_crossmapper/location.py | 2 +- mutalyzer_crossmapper/locus.py | 34 ++-- mutalyzer_crossmapper/multi_locus.py | 50 +++--- tests/test_crossmapper.py | 234 +++++++++++++-------------- tests/test_location.py | 36 ++--- tests/test_locus.py | 64 ++++---- tests/test_multi_locus.py | 154 +++++++++--------- 9 files changed, 302 insertions(+), 302 deletions(-) diff --git a/mutalyzer_crossmapper/__init__.py b/mutalyzer_crossmapper/__init__.py index 4f4109d..db521b2 100644 --- a/mutalyzer_crossmapper/__init__.py +++ b/mutalyzer_crossmapper/__init__.py @@ -1,7 +1,7 @@ from importlib.metadata import metadata from .crossmapper import Coding, Genomic, NonCoding, GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint -from .location import nearest_location +from .location import _nearest_location from .locus import Locus from .multi_locus import MultiLocus diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 217233e..328c1e3 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -2,7 +2,7 @@ from .multi_locus import MultiLocus, Point, _check_in_range, _check_multi_locus from .locus import Coord, _check_locus, _check_int -from .location import nearest_location +from .location import _nearest_location @dataclass(slots=True) class GenomicPoint: @@ -12,7 +12,7 @@ class GenomicPoint: def __post_init__(self) -> None: _check_int(self.position) if self.position <= 0: - raise ValueError(f"Position {self.position} must be a positive integer.") + raise ValueError(f'Position {self.position} must be a positive integer.') def __str__(self) -> str: return f'{self.position}' @@ -54,7 +54,7 @@ def __post_init__(self) -> None: _check_int(self.offset) if self.region not in self.allowed_regions: - raise ValueError(f"Region must be a string in {self.allowed_regions}.") + raise ValueError(f'Region must be a string in {self.allowed_regions}.') def __str__(self) -> str: if self.offset == 0: @@ -107,11 +107,11 @@ def noncoding_to_coordinate(self, point: NonCodingPoint) -> Coord: ) ) except ValueError as e: - if "Position" in str(e): + if 'Position' in str(e): raise ValueError(str(e).replace(str(point.position - 1), str(point.position))) raise e except IndexError as e: - if "Position" in str(e): + if 'Position' in str(e): raise IndexError(str(e).replace(str(point.position - 1), str(point.position))) raise e @@ -185,9 +185,9 @@ def _check_cds(self, cds: tuple[int, int], locations: list[tuple[int, int]], len _check_locus(cds) _check_in_range(cds[1], length) for coord in cds: - index = nearest_location(locations, coord) + index = _nearest_location(locations, coord) if coord < locations[index][0] or coord > locations[index][1]: - raise ValueError(f"Coordinate {coord} of CDS {cds} is not within any exon.") + raise ValueError(f'Coordinate {coord} of CDS {cds} is not within any exon.') def _validate_point(self, position: int, region: str) -> None: """Validate a coding point model under HGVS rules. @@ -196,19 +196,19 @@ def _validate_point(self, position: int, region: str) -> None: """ if region == 'u': if position not in (1, self._coding[0]): - raise ValueError(f"Position {position} is not in upstream boundary.") + raise ValueError(f'Position {position} is not in upstream boundary.') if region == '-': if position not in range(1, self._coding[0] + 1): - raise ValueError(f"Position {position} exceeds - region.") + raise ValueError(f'Position {position} exceeds - region.') if region == '': if position not in range(1, self._coding[1] - self._coding[0] + 1): - raise ValueError(f"Position {position} exceeds coding region.") + raise ValueError(f'Position {position} exceeds coding region.') if region == '*': if position not in range(1, self._exons[1] - self._coding[1] + 1): - raise ValueError(f"Position {position} exceeds * region.") + raise ValueError(f'Position {position} exceeds * region.') if region == 'd': if position not in (1, self._coding[0], self._exons[1] - self._coding[1]): - raise ValueError(f"Position {position} is not in downstream boundary.") + raise ValueError(f'Position {position} is not in downstream boundary.') def _coordinate_to_coding(self, coord: Coord) -> CodingPoint: @@ -310,11 +310,11 @@ def _coding_to_coordinate(self, point: CodingPoint) -> Coord: Point(position=position, region='', offset=point.offset) ) except ValueError as e: - if "Position" in str(e): + if 'Position' in str(e): raise ValueError(str(e).replace(str(position), str(point.position))) raise e except IndexError as e: - if "Position" in str(e): + if 'Position' in str(e): raise IndexError(str(e).replace(str(position), str(point.position))) raise e diff --git a/mutalyzer_crossmapper/location.py b/mutalyzer_crossmapper/location.py index 9b534e4..30ffeaf 100644 --- a/mutalyzer_crossmapper/location.py +++ b/mutalyzer_crossmapper/location.py @@ -19,7 +19,7 @@ def _nearest_boundary(lb: int, rb: int, c: int, p: int) -> int: return p -def nearest_location(ls: list[tuple[int, int]], c: int, p: int = 0) -> int: +def _nearest_location(ls: list[tuple[int, int]], c: int, p: int = 0) -> int: """Find the location nearest to `c`. In case of a draw, the parameter `p` decides which index is chosen. diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index d859a58..4103dfe 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -3,7 +3,7 @@ @dataclass(slots=True) class Point: - """Locus point dataclass""" + """Locus point dataclass.""" position: int offset: int = 0 @@ -14,7 +14,7 @@ def __post_init__(self) -> None: @dataclass(slots=True) class Coord: - """Coordinate dataclass""" + """Coordinate dataclass.""" coordinate: int def __post_init__(self) -> None: @@ -24,26 +24,26 @@ def __post_init__(self) -> None: def _check_int(value: int) -> None: """Check if the value type is integer.""" if not isinstance(value, int): - raise ValueError("Value must be an integer.") + raise ValueError('Value must be an integer.') def _check_non_negative_int(value: int) -> None: """Check if the coordinate is a non-negative integer.""" _check_int(value) if value < 0: - raise ValueError("Value must be non-negative.") + raise ValueError('Value must be non-negative.') def _check_locus(locus: tuple[int, int]) -> None: """Check if the range is valid.""" if not isinstance(locus, tuple) or len(locus) != 2: - raise ValueError("Locus must be a tuple of two values.") + raise ValueError('Locus must be a tuple of two values.') for value in locus: _check_non_negative_int(value) if locus[0] >= locus[1]: - raise ValueError(f"Locus start {locus[0]} must be smaller than locus end {locus[1]}.") + raise ValueError(f'Locus start {locus[0]} must be smaller than locus end {locus[1]}.') class Locus(object): @@ -61,26 +61,26 @@ def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: self._end = location[1] - location[0] # one-based length of the locus def _validate_point(self, position: int, offset: int) -> None: - """Validate a point according to HGVS rules. + """Validate a locus Point dataclass according to HGVS rules. :arg int position: Position. :arg int offset: Offset. """ if offset != 0 and position not in (0, self._end-1): - raise ValueError(f"Position {position} is not at a locus boundary.") + raise ValueError(f'Position {position} is not at a locus boundary.') if offset < 0 and position != 0: - raise IndexError(f"Offset {offset} should be at a locus start.") + raise IndexError(f'Offset {offset} should be at a locus start.') if offset > 0 and position != self._end-1: - raise IndexError(f"Offset {offset} should be at a locus end.") + raise IndexError(f'Offset {offset} should be at a locus end.') if position > self._end-1: - raise IndexError(f"Position {position} exceeds locus length.") + raise IndexError(f'Position {position} exceeds locus length.') def to_position(self, coord: Coord) -> Point: - """Convert a coordinate to a proper point model. + """Convert a coordinate dataclass to a locus point dataclass. - :arg Coord coord: Coordinate module. + :arg Coord coord: Coordinate dataclass. - :returns Point: Position point model. + :returns Point: Locus point dataclass. """ if self._inverted: if coord.coordinate > self.boundary[1]: @@ -96,11 +96,11 @@ def to_position(self, coord: Coord) -> Point: return Point(position=coord.coordinate - self.boundary[0], offset=0) def to_coordinate(self, point: Point) -> Coord: - """Convert a point model to a coordinate model. + """Convert a locus dataclass point model to a coordinate dataclass. - :arg Point point: Point model. + :arg Point point: Locus point dataclass. - :returns Coord: Coordinate model. + :returns Coord: Coordinate dataclass. """ self._validate_point(point.position, point.offset) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 41974bb..5709252 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -3,7 +3,7 @@ from dataclasses import dataclass -from .location import nearest_location +from .location import _nearest_location from .locus import Locus, Coord, _check_locus from .locus import Point as LocusPoint @@ -16,25 +16,25 @@ class Point(LocusPoint): def __post_init__(self) -> None: LocusPoint.__post_init__(self) if self.region not in ('', 'u', 'd'): - raise ValueError(f"Region {self.region} is not valid. Must be '', 'u', or 'd'.") + raise ValueError(f'Region {self.region} is not valid. Must be "", "u", or "d".') def _check_in_range(value: int, length: int | None = None) -> None: """Check if the value no larger than length.""" if length is not None and value >= length: - raise ValueError(f"Value {value} must be within the bounds of the reference length {length}.") + raise ValueError(f'Value {value} must be within the bounds of the reference length {length}.') def _check_multi_locus(locations: list[tuple[int, int]], length: int | None = None) -> None: """Check if the locations list is valid.""" if not locations or not isinstance(locations, list): - raise ValueError("Locations must be a non-empty list of tuples.") + raise ValueError('Locations must be a non-empty list of tuples.') for locus in locations: _check_locus(locus) for l1, l2 in zip(locations, locations[1:]): if l2[0] < l1[1]: - raise ValueError(f"Locus {l2} and locus {l1} are overlapping.") + raise ValueError(f'Locus {l2} and locus {l1} are overlapping.') _check_in_range(locations[-1][1], length) @@ -84,28 +84,28 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - # Upstream region validation, position is constant value and offset should not be positive if region == 'u': if position != self._offsets[0]: - raise ValueError(f"Position {position} is not at upstream boundary.") + raise ValueError(f'Position {position} is not at upstream boundary.') if offset >= 0: - raise ValueError(f"Offset {offset} at upstream boundary should be negative.") + raise ValueError(f'Offset {offset} at upstream boundary should be negative.') if self._inverted: if self._length is not None and -offset >= self._length - self._loci[self._direction(0)].boundary[1]: - raise ValueError(f"Offset {offset} exceeds upstream region.") + raise ValueError(f'Offset {offset} exceeds upstream region.') else: if -offset > self._loci[self._direction(0)].boundary[0]: - raise ValueError(f"Offset {offset} exceeds upstream region.") + raise ValueError(f'Offset {offset} exceeds upstream region.') # Downstream region validation, position is constant value and offset should not be negative if region == 'd': if position != self._end-1: - raise ValueError(f"Position {position} is not at downstream boundary.") + raise ValueError(f'Position {position} is not at downstream boundary.') if offset <= 0: - raise ValueError(f"Offset {offset} at downstream boundary should be positive.") + raise ValueError(f'Offset {offset} at downstream boundary should be positive.') if not self._inverted: if self._length is not None and offset >= self._length - self._loci[self._direction(-1)].boundary[1]: - raise ValueError(f"Offset {offset} exceeds downstream region.") + raise ValueError(f'Offset {offset} exceeds downstream region.') else: if offset > self._loci[self._direction(len(self._locations)-1)].boundary[0]: - raise ValueError(f"Offset {offset} exceeds downstream region.") + raise ValueError(f'Offset {offset} exceeds downstream region.') # '' region validation, position should be within the MultiLocus and offset should not exceed intron length if region == '': @@ -114,20 +114,20 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - if self._direction(index) == len(self._loci) - 1: if position == 0: raise ValueError( - f"Offset {offset} at the first exon on the reverse complement should be in the upstream region." + f'Offset {offset} at the first exon on the reverse complement should be in the upstream region.' ) else: if -offset >= self._loci[self._direction(index-1)].boundary[0] - self._loci[self._direction(index)].boundary[1]: - raise IndexError(f"Offset {offset} exceeds intron length.") + raise IndexError(f'Offset {offset} exceeds intron length.') if offset > 0: if self._direction(index) == 0: if position == self._end-1: raise ValueError( - f"Offset {offset} at the first exon on the reverse complement should be in the downstream region." + f'Offset {offset} at the first exon on the reverse complement should be in the downstream region.' ) else: if offset >= self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index+1)].boundary[1]: - raise IndexError(f"Offset {offset} exceeds intron length.") + raise IndexError(f'Offset {offset} exceeds intron length.') if not self._inverted: @@ -135,20 +135,20 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - if self._direction(index) == 0: if position == 0: raise ValueError( - f"Offset {offset} at the first exon should be in the upstream region." + f'Offset {offset} at the first exon should be in the upstream region.' ) else: if -offset >= self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index-1)].boundary[1]: - raise IndexError(f"Offset {offset} exceeds intron length.") + raise IndexError(f'Offset {offset} exceeds intron length.') if offset > 0: if self._direction(index) == len(self._loci) - 1: if position == self._end-1: raise ValueError( - f"Offset {offset} at the last exon should be in the downstream region." + f'Offset {offset} at the last exon should be in the downstream region.' ) else: if offset >= self._loci[self._direction(index+1)].boundary[0] - self._loci[self._direction(index)].boundary[1]: - raise IndexError(f"Offset {offset} exceeds intron length.") + raise IndexError(f'Offset {offset} exceeds intron length.') def _direction(self, index: int) -> int: @@ -177,7 +177,7 @@ def to_position(self, coord: Coord) -> Point: :returns Point: Point model. """ self._validate_coord(coord.coordinate) - index = nearest_location(self._locations, coord.coordinate, self._inverted) + index = _nearest_location(self._locations, coord.coordinate, self._inverted) outside = self._orientation * self._outside(coord.coordinate) region = 'u' if outside < 0 else 'd' if outside > 0 else '' point = self._loci[index].to_position(coord) @@ -219,13 +219,13 @@ def to_coordinate(self, point: Point) -> Coord: ) except ValueError as e: - if "Position" in str(e): + if 'Position' in str(e): raise ValueError(str(e).replace(str(point.position - self._offsets[index]), str(point.position))) from e raise except IndexError as e: - if "Position" in str(e): + if 'Position' in str(e): raise IndexError( - f"Position {point.position} exceeds multi locus length." + f'Position {point.position} exceeds multi locus length.' ) from e raise diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index ced10f2..8ce1687 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -11,13 +11,13 @@ def test_GenomicPoint_invalid_initialization(): """GenomicPoint cannot be initialized with invalid position.""" with pytest.raises(ValueError) as e: GenomicPoint(position=0) - assert str(e.value) == "Position 0 must be a positive integer." + assert str(e.value) == 'Position 0 must be a positive integer.' with pytest.raises(ValueError) as e: GenomicPoint(position=-1) - assert str(e.value) == "Position -1 must be a positive integer." + assert str(e.value) == 'Position -1 must be a positive integer.' with pytest.raises(ValueError) as e: GenomicPoint(position=[101]) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' def test_Genomic(): @@ -43,10 +43,10 @@ def test_Genomic_invalid_with_length(): crossmap = Genomic() with pytest.raises(ValueError) as e: crossmap.coordinate_to_genomic(Coord(-1), 99) - assert str(e.value) == "Value must be non-negative." + assert str(e.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as e: crossmap.coordinate_to_genomic(Coord(99), 99) - assert str(e.value) == "Value 99 must be within the bounds of the reference length 99." + assert str(e.value) == 'Value 99 must be within the bounds of the reference length 99.' def test_Genomic_with_length(): @@ -71,22 +71,22 @@ def test_NonCodingPoint_invalid_initialization(): """Raise error with invalid initialization.""" with pytest.raises(ValueError) as e: NonCodingPoint(position=0, offset=0, region='u') - assert str(e.value) == "Position 0 must be a positive integer." + assert str(e.value) == 'Position 0 must be a positive integer.' with pytest.raises(ValueError) as e: NonCodingPoint(position=0, offset=0, region='d') - assert str(e.value) == "Position 0 must be a positive integer." + assert str(e.value) == 'Position 0 must be a positive integer.' with pytest.raises(ValueError) as e: NonCodingPoint(position=0, offset=0, region='') - assert str(e.value) == "Position 0 must be a positive integer." + assert str(e.value) == 'Position 0 must be a positive integer.' with pytest.raises(ValueError) as e: NonCodingPoint(position=0, offset=0, region='*') - assert str(e.value) == "Position 0 must be a positive integer." + assert str(e.value) == 'Position 0 must be a positive integer.' with pytest.raises(ValueError) as e: NonCodingPoint(position=-1, offset=0, region='') - assert str(e.value) == "Position -1 must be a positive integer." + assert str(e.value) == 'Position -1 must be a positive integer.' with pytest.raises(ValueError) as e: NonCodingPoint(position=1, offset=None, region='u') - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: NonCodingPoint(position=1, offset=1, region='-') assert str(e.value) == "Region must be a string in ['', 'u', 'd']." @@ -96,47 +96,47 @@ def test_NonCoding_invalid(): """Raise ValueError if noncoding is invalid.""" with pytest.raises(ValueError) as e: NonCoding([()]) - assert str(e.value) == "Locus must be a tuple of two values." + assert str(e.value) == 'Locus must be a tuple of two values.' with pytest.raises(ValueError) as e: NonCoding([(10)]) - assert str(e.value) == "Locus must be a tuple of two values." + assert str(e.value) == 'Locus must be a tuple of two values.' with pytest.raises(ValueError) as e: NonCoding([(10, 20), (15, 25)]) - assert str(e.value) == "Locus (15, 25) and locus (10, 20) are overlapping." + assert str(e.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' with pytest.raises(ValueError) as e: NonCoding([(None, 20), (30, None)]) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: NonCoding(_exons, length=70) - assert str(e.value) == "Value 72 must be within the bounds of the reference length 70." + assert str(e.value) == 'Value 72 must be within the bounds of the reference length 70.' # Reverse orientation with pytest.raises(ValueError) as e: NonCoding([()], inverted=True) - assert str(e.value) == "Locus must be a tuple of two values." + assert str(e.value) == 'Locus must be a tuple of two values.' with pytest.raises(ValueError) as e: NonCoding([(10)], inverted=True) - assert str(e.value) == "Locus must be a tuple of two values." + assert str(e.value) == 'Locus must be a tuple of two values.' with pytest.raises(ValueError) as e: NonCoding([(10, 20), (15, 25)], inverted=True) - assert str(e.value) == "Locus (15, 25) and locus (10, 20) are overlapping." + assert str(e.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' with pytest.raises(ValueError) as e: NonCoding([(None, 20), (30, None)], inverted=True) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: NonCoding(_exons, length=70, inverted=True) - assert str(e.value) == "Value 72 must be within the bounds of the reference length 70." + assert str(e.value) == 'Value 72 must be within the bounds of the reference length 70.' def test_NonCoding_invalid_with_length(): """Raise ValueError if coordinate is out of bounds.""" with pytest.raises(ValueError) as e: NonCoding(_exons, length=70) - assert str(e.value) == "Value 72 must be within the bounds of the reference length 70." + assert str(e.value) == 'Value 72 must be within the bounds of the reference length 70.' # Reverse orientation with pytest.raises(ValueError) as e: NonCoding(_exons, length=70, inverted=True) - assert str(e.value) == "Value 72 must be within the bounds of the reference length 70." + assert str(e.value) == 'Value 72 must be within the bounds of the reference length 70.' def test_NonCoding(): @@ -225,10 +225,10 @@ def test_NonCoding_with_length(): ) with pytest.raises(ValueError) as e: crossmap.coordinate_to_noncoding(Coord(75)) - assert str(e.value) == "Value 75 must be within the bounds of the reference length 75." + assert str(e.value) == 'Value 75 must be within the bounds of the reference length 75.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=4, region='d')) - assert str(e.value) == "Offset 4 exceeds downstream region." + assert str(e.value) == 'Offset 4 exceeds downstream region.' def test_NonCoding_inverted(): @@ -271,10 +271,10 @@ def test_NonCoding_inverted_with_length(): # Boundary between upstream and sequence end. with pytest.raises(ValueError) as e: crossmap.coordinate_to_noncoding(Coord(75)) - assert str(e.value) == "Value 75 must be within the bounds of the reference length 75." + assert str(e.value) == 'Value 75 must be within the bounds of the reference length 75.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-4, region='u')) - assert str(e.value) == "Offset -4 exceeds upstream region." + assert str(e.value) == 'Offset -4 exceeds upstream region.' invariant( crossmap.coordinate_to_noncoding, Coord(74), @@ -316,19 +316,19 @@ def test_NonCoding_invalid_position(): crossmap = NonCoding(_exons, length=75) with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=0, offset=1, region='u')) - assert str(e.value) == "Position 0 must be a positive integer." + assert str(e.value) == 'Position 0 must be a positive integer.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=2, offset=1, region='u')) - assert str(e.value) == "Position 2 is not at upstream boundary." + assert str(e.value) == 'Position 2 is not at upstream boundary.' with pytest.raises(IndexError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=23, offset=0, region='')) - assert str(e.value) == "Position 23 exceeds multi locus length." + assert str(e.value) == 'Position 23 exceeds multi locus length.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=21, offset=1, region='d')) - assert str(e.value) == "Position 21 is not at downstream boundary." + assert str(e.value) == 'Position 21 is not at downstream boundary.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=30, offset=-1, region='d')) - assert str(e.value) == "Position 30 is not at downstream boundary." + assert str(e.value) == 'Position 30 is not at downstream boundary.' def test_NonCoding_invalid_position_inverted(): @@ -336,19 +336,19 @@ def test_NonCoding_invalid_position_inverted(): crossmap = NonCoding(_exons, length=75, inverted=True) with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=0, offset=1, region='u')) - assert str(e.value) == "Position 0 must be a positive integer." + assert str(e.value) == 'Position 0 must be a positive integer.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=2, offset=1, region='u')) - assert str(e.value) == "Position 2 is not at upstream boundary." + assert str(e.value) == 'Position 2 is not at upstream boundary.' with pytest.raises(IndexError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=23, offset=0, region='')) - assert str(e.value) == "Position 23 exceeds multi locus length." + assert str(e.value) == 'Position 23 exceeds multi locus length.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=21, offset=1, region='d')) - assert str(e.value) == "Position 21 is not at downstream boundary." + assert str(e.value) == 'Position 21 is not at downstream boundary.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=30, offset=-1, region='d')) - assert str(e.value) == "Position 30 is not at downstream boundary." + assert str(e.value) == 'Position 30 is not at downstream boundary.' def test_NonCoding_invalid_offset(): @@ -356,37 +356,37 @@ def test_NonCoding_invalid_offset(): crossmap = NonCoding(_exons, length=75) with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=0, region='u')) - assert e.value.args[0] == "Offset 0 at upstream boundary should be negative." + assert e.value.args[0] == 'Offset 0 at upstream boundary should be negative.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=1, region='u')) - assert e.value.args[0] == "Offset 1 at upstream boundary should be negative." + assert e.value.args[0] == 'Offset 1 at upstream boundary should be negative.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-6, region='u')) - assert e.value.args[0] == "Offset -6 exceeds upstream boundary." + assert e.value.args[0] == 'Offset -6 exceeds upstream boundary.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-1, region='')) - assert e.value.args[0] == "Offset -1 at the first exon should be in the upstream region." + assert e.value.args[0] == 'Offset -1 at the first exon should be in the upstream region.' with pytest.raises(IndexError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=1, region='')) - assert e.value.args[0] == "Offset 1 should be at a locus end." + assert e.value.args[0] == 'Offset 1 should be at a locus end.' with pytest.raises(IndexError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=10, offset=1, region='')) - assert e.value.args[0] == "Offset 1 should be at a locus end." + assert e.value.args[0] == 'Offset 1 should be at a locus end.' with pytest.raises(IndexError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=10, offset=-11, region='')) - assert e.value.args[0] == "Offset -11 exceeds intron length." + assert e.value.args[0] == 'Offset -11 exceeds intron length.' with pytest.raises(IndexError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=-1, region='')) - assert e.value.args[0] == "Offset -1 should be at a locus start." + assert e.value.args[0] == 'Offset -1 should be at a locus start.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=1, region='')) - assert e.value.args[0] == "Offset 1 at the first exon should be in the downstream region." + assert e.value.args[0] == 'Offset 1 at the first exon should be in the downstream region.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=0, region='d')) - assert e.value.args[0] == "Offset 0 at downstream boundary should be positive." + assert e.value.args[0] == 'Offset 0 at downstream boundary should be positive.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=-1, region='d')) - assert e.value.args[0] == "Offset -1 at downstream boundary should be positive." + assert e.value.args[0] == 'Offset -1 at downstream boundary should be positive.' def test_NonCoding_invalid_offset_inverted(): @@ -394,56 +394,56 @@ def test_NonCoding_invalid_offset_inverted(): crossmap = NonCoding(_exons, length=75, inverted=True) with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=0, region='u')) - assert e.value.args[0] == "Offset 0 at upstream boundary should be negative." + assert e.value.args[0] == 'Offset 0 at upstream boundary should be negative.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=1, region='u')) - assert e.value.args[0] == "Offset 1 at upstream boundary should be negative." + assert e.value.args[0] == 'Offset 1 at upstream boundary should be negative.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-5, region='u')) - assert e.value.args[0] == "Offset -5 exceeds upstream boundary." + assert e.value.args[0] == 'Offset -5 exceeds upstream boundary.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-1, region='')) - assert e.value.args[0] == "Offset -1 at the first exon should be in the upstream region." + assert e.value.args[0] == 'Offset -1 at the first exon should be in the upstream region.' with pytest.raises(IndexError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=1, region='')) - assert e.value.args[0] == "Offset 1 should be at a locus end." + assert e.value.args[0] == 'Offset 1 should be at a locus end.' with pytest.raises(IndexError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=13, offset=-1, region='')) - assert e.value.args[0] == "Offset -1 should be at a locus end." + assert e.value.args[0] == 'Offset -1 should be at a locus end.' with pytest.raises(IndexError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=13, offset=11, region='')) - assert e.value.args[0] == "Offset 11 exceeds intron length." + assert e.value.args[0] == 'Offset 11 exceeds intron length.' with pytest.raises(IndexError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=-1, region='')) - assert e.value.args[0] == "Offset -1 should be at a locus start." + assert e.value.args[0] == 'Offset -1 should be at a locus start.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=1, region='')) - assert e.value.args[0] == "Offset 1 at the last exon on the reverse complement should be in the downstream region." + assert e.value.args[0] == 'Offset 1 at the last exon on the reverse complement should be in the downstream region.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=0, region='d')) - assert e.value.args[0] == "Offset 0 at downstream boundary should be positive." + assert e.value.args[0] == 'Offset 0 at downstream boundary should be positive.' with pytest.raises(ValueError) as e: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=-1, region='d')) - assert e.value.args[0] == "Offset -1 at downstream boundary should be positive." + assert e.value.args[0] == 'Offset -1 at downstream boundary should be positive.' def test_CodingPoint_invalid_initialization(): """Raise error with invalid initialization.""" with pytest.raises(ValueError) as e: CodingPoint(position=0, offset=0, region='-') - assert str(e.value) == "Position 0 must be a positive integer." + assert str(e.value) == 'Position 0 must be a positive integer.' with pytest.raises(ValueError) as e: CodingPoint(position=0, offset=0, region='*') - assert str(e.value) == "Position 0 must be a positive integer." + assert str(e.value) == 'Position 0 must be a positive integer.' with pytest.raises(ValueError) as e: CodingPoint(position=0, offset=0, region='') - assert str(e.value) == "Position 0 must be a positive integer." + assert str(e.value) == 'Position 0 must be a positive integer.' with pytest.raises(ValueError) as e: CodingPoint(position=-1, offset=0, region='') - assert str(e.value) == "Position -1 must be a positive integer." + assert str(e.value) == 'Position -1 must be a positive integer.' with pytest.raises(ValueError) as e: CodingPoint(position=1, offset=None, region='') - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: CodingPoint(position=2, offset=1, region='upstream') assert str(e.value) == "Region must be a string in ['', 'u', 'd', '-', '*']." @@ -454,47 +454,47 @@ def test_Coding_invalid(): with pytest.raises(ValueError) as e: Coding([(20, 20)], (20, 20)) - assert str(e.value) == "Locus start 20 must be smaller than locus end 20." + assert str(e.value) == 'Locus start 20 must be smaller than locus end 20.' with pytest.raises(ValueError) as e: Coding([(10, 20)], (9,15)) - assert str(e.value) == "Coordinate 9 of CDS (9, 15) is not within any exon." + assert str(e.value) == 'Coordinate 9 of CDS (9, 15) is not within any exon.' with pytest.raises(ValueError) as e: Coding([(10, 20)], (10,21)) - assert str(e.value) == "Coordinate 21 of CDS (10, 21) is not within any exon." + assert str(e.value) == 'Coordinate 21 of CDS (10, 21) is not within any exon.' with pytest.raises(ValueError) as e: Coding([(10, 20)], (15, 10)) - assert str(e.value) == "Locus start 15 must be smaller than locus end 10." + assert str(e.value) == 'Locus start 15 must be smaller than locus end 10.' with pytest.raises(ValueError) as e: Coding([], None) - assert str(e.value) == "Locations must be a non-empty list of tuples." + assert str(e.value) == 'Locations must be a non-empty list of tuples.' # Reverse orientation with pytest.raises(ValueError) as e: Coding([(20, 20)], (20, 20), inverted=True) - assert str(e.value) == "Locus start 20 must be smaller than locus end 20." + assert str(e.value) == 'Locus start 20 must be smaller than locus end 20.' with pytest.raises(ValueError) as e: Coding([(10, 20)], (9,15), inverted=True) - assert str(e.value) == "Coordinate 9 of CDS (9, 15) is not within any exon." + assert str(e.value) == 'Coordinate 9 of CDS (9, 15) is not within any exon.' with pytest.raises(ValueError) as e: Coding([(10, 20)], (10,21), inverted=True) - assert str(e.value) == "Coordinate 21 of CDS (10, 21) is not within any exon." + assert str(e.value) == 'Coordinate 21 of CDS (10, 21) is not within any exon.' with pytest.raises(ValueError) as e: Coding([(10, 20)], (15, 10), inverted=True) - assert str(e.value) == "Locus start 15 must be smaller than locus end 10." + assert str(e.value) == 'Locus start 15 must be smaller than locus end 10.' with pytest.raises(ValueError) as e: Coding([], None, inverted=True) - assert str(e.value) == "Locations must be a non-empty list of tuples." + assert str(e.value) == 'Locations must be a non-empty list of tuples.' def test_Coding_invalid_with_length(): """Raise ValueError if coordinate is out of bounds.""" with pytest.raises(ValueError) as e: Coding(_exons, _cds, length=70) - assert str(e.value) == "Value 72 must be within the bounds of the reference length 70." + assert str(e.value) == 'Value 72 must be within the bounds of the reference length 70.' # Reverse orientation with pytest.raises(ValueError) as e: Coding(_exons, _cds, length=70, inverted=True) - assert str(e.value) == "Value 72 must be within the bounds of the reference length 70." + assert str(e.value) == 'Value 72 must be within the bounds of the reference length 70.' def test_Coding(): @@ -698,10 +698,10 @@ def test_Coding_inverted_with_length(): # Boundary between upstream and sequence end. with pytest.raises(ValueError) as e: crossmap.coordinate_to_coding(Coord(75)) - assert str(e.value) == "Value 75 must be within the bounds of the reference length 75." + assert str(e.value) == 'Value 75 must be within the bounds of the reference length 75.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=5, offset=-4, region='u')) - assert str(e.value) == "Offset -4 exceeds upstream region." + assert str(e.value) == 'Offset -4 exceeds upstream region.' invariant( crossmap.coordinate_to_coding, Coord(74), @@ -1281,34 +1281,34 @@ def test_Coding_invalid_position(): with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=0, offset=1, region='u')) - assert str(e.value) == "Position 0 must be a positive integer." + assert str(e.value) == 'Position 0 must be a positive integer.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=12, offset=-1, region='u')) - assert str(e.value) == "Position 12 is not in upstream boundary." + assert str(e.value) == 'Position 12 is not in upstream boundary.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=13, offset=1, region='-')) - assert str(e.value) == "Position 13 exceeds - region." + assert str(e.value) == 'Position 13 exceeds - region.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=-1, offset=0, region='-')) - assert str(e.value) == "Position -1 must be a positive integer." + assert str(e.value) == 'Position -1 must be a positive integer.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=0, offset=0, region='')) - assert str(e.value) == "Position 0 must be a positive integer." + assert str(e.value) == 'Position 0 must be a positive integer.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=7, offset=0, region='')) - assert str(e.value) == "Position 7 exceeds coding region." + assert str(e.value) == 'Position 7 exceeds coding region.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=6, offset=1, region='*')) - assert str(e.value) == "Position 6 exceeds * region." + assert str(e.value) == 'Position 6 exceeds * region.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=None, offset=0, region='*')) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=6, offset=0, region='d')) - assert str(e.value) == "Position 6 is not in downstream boundary." + assert str(e.value) == 'Position 6 is not in downstream boundary.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=1000, offset=2, region='d')) - assert str(e.value) == "Position 1000 is not in downstream boundary." + assert str(e.value) == 'Position 1000 is not in downstream boundary.' def test_Coding_inverted_invalid_position_inverted(): @@ -1317,34 +1317,34 @@ def test_Coding_inverted_invalid_position_inverted(): with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=0, offset=1, region='u')) - assert str(e.value) == "Position 0 must be a positive integer." + assert str(e.value) == 'Position 0 must be a positive integer.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=12, offset=-1, region='u')) - assert str(e.value) == "Position 12 is not in upstream boundary." + assert str(e.value) == 'Position 12 is not in upstream boundary.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=13, offset=1, region='-')) - assert str(e.value) == "Position 13 exceeds - region." + assert str(e.value) == 'Position 13 exceeds - region.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=-1, offset=0, region='-')) - assert str(e.value) == "Position -1 must be a positive integer." + assert str(e.value) == 'Position -1 must be a positive integer.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=0, offset=0, region='')) - assert str(e.value) == "Position 0 must be a positive integer." + assert str(e.value) == 'Position 0 must be a positive integer.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=7, offset=0, region='')) - assert str(e.value) == "Position 7 exceeds coding region." + assert str(e.value) == 'Position 7 exceeds coding region.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=13, offset=1, region='*')) - assert str(e.value) == "Position 13 exceeds * region." + assert str(e.value) == 'Position 13 exceeds * region.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=None, offset=0, region='*')) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=6, offset=0, region='d')) - assert str(e.value) == "Position 6 is not in downstream boundary." + assert str(e.value) == 'Position 6 is not in downstream boundary.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=1000, offset=2, region='d')) - assert str(e.value) == "Position 1000 is not in downstream boundary." + assert str(e.value) == 'Position 1000 is not in downstream boundary.' def test_Coding_invalid_offset(): @@ -1353,28 +1353,28 @@ def test_Coding_invalid_offset(): with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-6, region='u')) - assert str(e.value) == "Offset -6 exceeds upstream region." + assert str(e.value) == 'Offset -6 exceeds upstream region.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=1, region='-')) - assert str(e.value) == "Position 1 is not at a locus boundary." + assert str(e.value) == 'Position 1 is not at a locus boundary.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-1, region='-')) - assert str(e.value) == "Position 1 is not at a locus boundary." + assert str(e.value) == 'Position 1 is not at a locus boundary.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-1, region='')) - assert str(e.value) == "Position 1 is not at a locus boundary." + assert str(e.value) == 'Position 1 is not at a locus boundary.' with pytest.raises(IndexError) as e: crossmap.coding_to_coordinate(CodingPoint(position=3, offset=6, region='')) - assert str(e.value) == "Offset 6 exceeds intron length." + assert str(e.value) == 'Offset 6 exceeds intron length.' with pytest.raises(IndexError) as e: crossmap.coding_to_coordinate(CodingPoint(position=4, offset=12, region='*')) - assert str(e.value) == "Offset 12 should be at a locus end." + assert str(e.value) == 'Offset 12 should be at a locus end.' with pytest.raises(IndexError) as e: crossmap.coding_to_coordinate(CodingPoint(position=4, offset=-50, region='*')) - assert str(e.value) == "Offset -50 exceeds intron length." + assert str(e.value) == 'Offset -50 exceeds intron length.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=5, offset=10, region='d')) - assert str(e.value) == "Offset 10 exceeds downstream region." + assert str(e.value) == 'Offset 10 exceeds downstream region.' def test_Coding_invalid_offset_inverted(): @@ -1383,41 +1383,41 @@ def test_Coding_invalid_offset_inverted(): with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-6, region='u')) - assert str(e.value) == "Offset -6 exceeds upstream region." + assert str(e.value) == 'Offset -6 exceeds upstream region.' with pytest.raises(IndexError) as e: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=1, region='-')) - assert str(e.value) == "Offset 1 should be at a locus end." + assert str(e.value) == 'Offset 1 should be at a locus end.' with pytest.raises(IndexError) as e: crossmap.coding_to_coordinate(CodingPoint(position=2, offset=-1, region='-')) - assert str(e.value) == "Offset -1 should be at a locus start." + assert str(e.value) == 'Offset -1 should be at a locus start.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-1, region='')) - assert str(e.value) == "Position 1 is not at a locus boundary." + assert str(e.value) == 'Position 1 is not at a locus boundary.' with pytest.raises(IndexError) as e: crossmap.coding_to_coordinate(CodingPoint(position=3, offset=6, region='')) - assert str(e.value) == "Offset 6 exceeds intron length." + assert str(e.value) == 'Offset 6 exceeds intron length.' with pytest.raises(IndexError) as e: crossmap.coding_to_coordinate(CodingPoint(position=4, offset=12, region='*')) - assert str(e.value) == "Offset 12 exceeds intron length." + assert str(e.value) == 'Offset 12 exceeds intron length.' with pytest.raises(IndexError) as e: crossmap.coding_to_coordinate(CodingPoint(position=4, offset=-50, region='*')) - assert str(e.value) == "Offset -50 exceeds intron length." + assert str(e.value) == 'Offset -50 exceeds intron length.' with pytest.raises(ValueError) as e: crossmap.coding_to_coordinate(CodingPoint(position=11, offset=10, region='d')) - assert str(e.value) == "Offset 10 exceeds downstream region." + assert str(e.value) == 'Offset 10 exceeds downstream region.' def test_Coding_protein_point_invalid_initialization(): """Raise error if protein point is initialized with invalid values.""" with pytest.raises(ValueError) as e: ProteinPoint(position=0, offset=0, region='u', position_in_codon=1) - assert str(e.value) == "Position 0 must be a positive integer." + assert str(e.value) == 'Position 0 must be a positive integer.' with pytest.raises(ValueError) as e: ProteinPoint(position=1, offset=0, region='', position_in_codon=4) - assert str(e.value) == "Position_in_codon must be 1, 2, or 3." + assert str(e.value) == 'Position_in_codon must be 1, 2, or 3.' with pytest.raises(ValueError) as e: ProteinPoint(position=1, offset=0, region='', position_in_codon=0) - assert str(e.value) == "Position_in_codon must be 1, 2, or 3." + assert str(e.value) == 'Position_in_codon must be 1, 2, or 3.' def test_Coding_protein(): diff --git a/tests/test_location.py b/tests/test_location.py index 0841ed3..f912463 100644 --- a/tests/test_location.py +++ b/tests/test_location.py @@ -1,4 +1,4 @@ -from mutalyzer_crossmapper import nearest_location +from mutalyzer_crossmapper import _nearest_location from mutalyzer_crossmapper.location import _nearest_boundary @@ -20,36 +20,36 @@ def test_nearest_location(): """Index of the nearest location.""" locations = [(10, 20), (30, 40), (50, 60)] - assert nearest_location(locations, 8) == 0 - assert nearest_location(locations, 15) == 0 - assert nearest_location(locations, 22) == 0 + assert _nearest_location(locations, 8) == 0 + assert _nearest_location(locations, 15) == 0 + assert _nearest_location(locations, 22) == 0 - assert nearest_location(locations, 28) == 1 - assert nearest_location(locations, 35) == 1 - assert nearest_location(locations, 42) == 1 + assert _nearest_location(locations, 28) == 1 + assert _nearest_location(locations, 35) == 1 + assert _nearest_location(locations, 42) == 1 - assert nearest_location(locations, 48) == 2 - assert nearest_location(locations, 55) == 2 - assert nearest_location(locations, 62) == 2 + assert _nearest_location(locations, 48) == 2 + assert _nearest_location(locations, 55) == 2 + assert _nearest_location(locations, 62) == 2 def test_nearest_location_even(): """Index of the nearest location, preference is irrelevant.""" - assert nearest_location([(3, 6), (8, 13)], 6, 0) == 0 - assert nearest_location([(3, 6), (8, 13)], 6, 1) == 0 - assert nearest_location([(3, 6), (8, 13)], 7, 0) == 1 - assert nearest_location([(3, 6), (8, 13)], 7, 1) == 1 + assert _nearest_location([(3, 6), (8, 13)], 6, 0) == 0 + assert _nearest_location([(3, 6), (8, 13)], 6, 1) == 0 + assert _nearest_location([(3, 6), (8, 13)], 7, 0) == 1 + assert _nearest_location([(3, 6), (8, 13)], 7, 1) == 1 def test_nearest_location_odd(): """Index of the nearest location, preference is relevant.""" - assert nearest_location([(3, 6), (9, 13)], 7) == 0 - assert nearest_location([(3, 6), (9, 13)], 7, 1) == 1 + assert _nearest_location([(3, 6), (9, 13)], 7) == 0 + assert _nearest_location([(3, 6), (9, 13)], 7, 1) == 1 def test_nearest_location_adjacent(): """Adjacent locations have no overlap.""" locations = [(1, 3), (3, 5)] - assert nearest_location(locations, 2) == 0 - assert nearest_location(locations, 3) == 1 + assert _nearest_location(locations, 2) == 0 + assert _nearest_location(locations, 3) == 1 diff --git a/tests/test_locus.py b/tests/test_locus.py index 59b2be7..efae896 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -8,67 +8,67 @@ def test_invalid_Locus_initialization(): """Test Locus initialization.""" with pytest.raises(ValueError) as e: Locus((10, 5)) - assert str(e.value) == "Locus start 10 must be smaller than locus end 5." + assert str(e.value) == 'Locus start 10 must be smaller than locus end 5.' with pytest.raises(ValueError) as e: Locus((10, 20, 30)) - assert str(e.value) == "Locus must be a tuple of two values." + assert str(e.value) == 'Locus must be a tuple of two values.' with pytest.raises(ValueError) as e: Locus((10, -5)) - assert str(e.value) == "Value must be non-negative." + assert str(e.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as e: Locus((10, 20.5)) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: Locus((10, None)) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: - Locus(("10", "20")) - assert str(e.value) == "Value must be an integer." + Locus(('10', '20')) + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: Locus((10, 10)) - assert str(e.value) == "Locus start 10 must be smaller than locus end 10." + assert str(e.value) == 'Locus start 10 must be smaller than locus end 10.' # Inverted Locus initialization with pytest.raises(ValueError) as e: Locus((10, 5), inverted=True) - assert str(e.value) == "Locus start 10 must be smaller than locus end 5." + assert str(e.value) == 'Locus start 10 must be smaller than locus end 5.' with pytest.raises(ValueError) as e: Locus((10, 20, 30), inverted=True) - assert str(e.value) == "Locus must be a tuple of two values." + assert str(e.value) == 'Locus must be a tuple of two values.' with pytest.raises(ValueError) as e: Locus((10, -5), inverted=True) - assert str(e.value) == "Value must be non-negative." + assert str(e.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as e: Locus((10, 20.5), inverted=True) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: Locus((10, None), inverted=True) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: - Locus(("10", "20"), inverted=True) - assert str(e.value) == "Value must be an integer." + Locus(('10', '20'), inverted=True) + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: Locus((10, 10), inverted=True) - assert str(e.value) == "Locus start 10 must be smaller than locus end 10." + assert str(e.value) == 'Locus start 10 must be smaller than locus end 10.' def test_invalid_Coord_initialization(): """Test Coord initialization.""" with pytest.raises(ValueError) as e: Coord(-1) - assert str(e.value) == "Value must be non-negative." + assert str(e.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as e: Coord(3.5) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: - Coord("10") - assert str(e.value) == "Value must be an integer." + Coord('10') + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: Coord(None) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: Coord([10]) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' def test_invalid_Locus_point(): @@ -76,20 +76,20 @@ def test_invalid_Locus_point(): locus = Locus((30, 35)) with pytest.raises(ValueError) as e: locus.to_coordinate(Point(position=-5, offset=0)) - assert str(e.value) == "Value must be non-negative." + assert str(e.value) == 'Value must be non-negative.' with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=5, offset=0)) - assert str(e.value) == "Position 5 exceeds locus length." + assert str(e.value) == 'Position 5 exceeds locus length.' with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=0, offset=2)) - assert str(e.value) == "Offset 2 should be at a locus end." + assert str(e.value) == 'Offset 2 should be at a locus end.' with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=4, offset=-2)) - assert str(e.value) == "Offset -2 should be at a locus start." + assert str(e.value) == 'Offset -2 should be at a locus start.' with pytest.raises(ValueError) as e: locus.to_coordinate(Point(position=2, offset=1)) - assert str(e.value) == "Position 2 is not at a locus boundary." + assert str(e.value) == 'Position 2 is not at a locus boundary.' def test_invalid_Locus_inverted_point(): @@ -97,19 +97,19 @@ def test_invalid_Locus_inverted_point(): locus = Locus((30, 35), True) with pytest.raises(ValueError) as e: locus.to_coordinate(Point(position=-5, offset=0)) - assert str(e.value) == "Value must be non-negative." + assert str(e.value) == 'Value must be non-negative.' with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=5, offset=0)) - assert str(e.value) == "Position 5 exceeds locus length." + assert str(e.value) == 'Position 5 exceeds locus length.' with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=0, offset=2)) - assert str(e.value) == "Offset 2 should be at a locus end." + assert str(e.value) == 'Offset 2 should be at a locus end.' with pytest.raises(IndexError) as e: locus.to_coordinate(Point(position=4, offset=-2)) - assert str(e.value) == "Offset -2 should be at a locus start." + assert str(e.value) == 'Offset -2 should be at a locus start.' with pytest.raises(ValueError) as e: locus.to_coordinate(Point(position=2, offset=1)) - assert str(e.value) == "Position 2 is not at a locus boundary." + assert str(e.value) == 'Position 2 is not at a locus boundary.' def test_Locus(): diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index da95e0b..dce3956 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -32,54 +32,54 @@ def test_invalid_MultiLocus_initialization(): """Test MultiLocus initialization.""" with pytest.raises(ValueError) as e: MultiLocus(([(10, 5), (20, 25)])) - assert str(e.value) == "Locus start 10 must be smaller than locus end 5." + assert str(e.value) == 'Locus start 10 must be smaller than locus end 5.' with pytest.raises(ValueError) as e: MultiLocus([(10, 20, 30), (40, 50)]) - assert str(e.value) == "Locus must be a tuple of two values." + assert str(e.value) == 'Locus must be a tuple of two values.' with pytest.raises(ValueError) as e: MultiLocus([(10, -5), (20, 25)]) - assert str(e.value) == "Value must be non-negative." + assert str(e.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as e: MultiLocus([(10, 20.5), (30, 40)]) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: MultiLocus([(10.5, None), (20, 30)]) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: - MultiLocus([("10", "20"), (30, 40)]) - assert str(e.value) == "Value must be an integer." + MultiLocus([('10', '20'), (30, 40)]) + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: MultiLocus([(10, 20), (15, 25)]) - assert str(e.value) == "Locus (15, 25) and locus (10, 20) are overlapping." + assert str(e.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' with pytest.raises(ValueError) as e: MultiLocus([(10, 12), (15, 25)], 25) - assert str(e.value) == "Value 25 must be within the bounds of the reference length 25." + assert str(e.value) == 'Value 25 must be within the bounds of the reference length 25.' # Inverted MultiLocus initialization with pytest.raises(ValueError) as e: MultiLocus(([(10, 5), (20, 25)]), inverted=True) - assert str(e.value) == "Locus start 10 must be smaller than locus end 5." + assert str(e.value) == 'Locus start 10 must be smaller than locus end 5.' with pytest.raises(ValueError) as e: MultiLocus([(10, 20, 30), (40, 50)], inverted=True) - assert str(e.value) == "Locus must be a tuple of two values." + assert str(e.value) == 'Locus must be a tuple of two values.' with pytest.raises(ValueError) as e: MultiLocus([(10, -5), (20, 25)], inverted=True) - assert str(e.value) == "Value must be non-negative." + assert str(e.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as e: MultiLocus([(10, 20.5), (30, 40)], inverted=True) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: MultiLocus([(10.5, None), (20, 30)], inverted=True) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: - MultiLocus([("10", "20"), (30, 40)], inverted=True) - assert str(e.value) == "Value must be an integer." + MultiLocus([('10', '20'), (30, 40)], inverted=True) + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: MultiLocus([(10, 20), (15, 25)], 25, inverted=True) - assert str(e.value) == "Locus (15, 25) and locus (10, 20) are overlapping." + assert str(e.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' with pytest.raises(ValueError) as e: MultiLocus([(10, 12), (15, 25)], 25, inverted=True) - assert str(e.value) == "Value 25 must be within the bounds of the reference length 25." + assert str(e.value) == 'Value 25 must be within the bounds of the reference length 25.' def test_MultiLocus_invalid_coordinate(): @@ -87,29 +87,29 @@ def test_MultiLocus_invalid_coordinate(): multi_locus = MultiLocus([(30, 35), (40, 45)]) with pytest.raises(ValueError) as e: multi_locus.to_position(Coord(-1)) - assert str(e.value) == "Value must be non-negative." + assert str(e.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as e: multi_locus.to_position(Coord(46.7)) - assert str(e.value) == "Value must be an integer." + assert str(e.value) == 'Value must be an integer.' with pytest.raises(ValueError) as e: - multi_locus.to_position(Coord("31")) - assert str(e.value) == "Value must be an integer." + multi_locus.to_position(Coord('31')) + assert str(e.value) == 'Value must be an integer.' def test_invalid_Point_initialization(): """Test Point initialization.""" with pytest.raises(ValueError) as e: Point(position=-1, offset=0, region='') - assert str(e.value) == "Value must be non-negative." + assert str(e.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as e: Point(position=0, offset=0, region='*') - assert str(e.value) == "Region * is not valid. Must be '', 'u', or 'd'." + assert str(e.value) == 'Region * is not valid. Must be "", "u", or "d".' with pytest.raises(ValueError) as e: Point(position=0, offset=0, region=None) - assert str(e.value) == "Region None is not valid. Must be '', 'u', or 'd'." + assert str(e.value) == 'Region None is not valid. Must be "", "u", or "d".' with pytest.raises(ValueError) as e: - Point(position="11", offset=0, region='u') - assert str(e.value) == "Value must be an integer." + Point(position='11', offset=0, region='u') + assert str(e.value) == 'Value must be an integer.' def test_MultiLocus(): @@ -508,196 +508,196 @@ def test_upstream_invalid_position(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=1, offset=-1, region='u')) - assert str(e.value) == "Position 1 is not at upstream boundary." + assert str(e.value) == 'Position 1 is not at upstream boundary.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=-1, offset=-1, region='u')) - assert str(e.value) == "Value must be non-negative." + assert str(e.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=20, offset=-1, region='u')) - assert str(e.value) == "Position 20 is not at upstream boundary." + assert str(e.value) == 'Position 20 is not at upstream boundary.' def test_upstream_invalid_position_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=1, offset=-1, region='u')) - assert str(e.value) == "Position 1 is not at upstream boundary." + assert str(e.value) == 'Position 1 is not at upstream boundary.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=-1, offset=-1, region='u')) - assert str(e.value) == "Value must be non-negative." + assert str(e.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=20, offset=-1, region='u')) - assert str(e.value) == "Position 20 is not at upstream boundary." + assert str(e.value) == 'Position 20 is not at upstream boundary.' def test_upstream_invalid_offset(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=1, region='u')) - assert str(e.value) == "Offset 1 at upstream boundary should be negative." + assert str(e.value) == 'Offset 1 at upstream boundary should be negative.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=-6, region='u')) - assert str(e.value) == "Offset -6 exceeds upstream region." + assert str(e.value) == 'Offset -6 exceeds upstream region.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=0, region='u')) - assert str(e.value) == "Offset 0 at upstream boundary should be negative." + assert str(e.value) == 'Offset 0 at upstream boundary should be negative.' def test_upstream_invalid_offset_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=1, region='u')) - assert str(e.value) == "Offset 1 at upstream boundary should be negative." + assert str(e.value) == 'Offset 1 at upstream boundary should be negative.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=-6, region='u')) - assert str(e.value) == "Offset -6 exceeds upstream region." + assert str(e.value) == 'Offset -6 exceeds upstream region.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=0, region='u')) - assert str(e.value) == "Offset 0 at upstream boundary should be negative." + assert str(e.value) == 'Offset 0 at upstream boundary should be negative.' def test_transcribed_invalid_position(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=-1, offset=0, region='')) - assert str(e.value) == "Value must be non-negative." + assert str(e.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=7, offset=1, region='')) - assert str(e.value) == "Position 7 is not at a locus boundary." + assert str(e.value) == 'Position 7 is not at a locus boundary.' with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=20, offset=0, region='')) - assert str(e.value) == "Position 20 exceeds multi locus length." + assert str(e.value) == 'Position 20 exceeds multi locus length.' def test_transcribed_invalid_position_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=-1, offset=0, region='')) - assert str(e.value) == "Value must be non-negative." + assert str(e.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=7, offset=-1, region='')) - assert str(e.value) == "Position 7 is not at a locus boundary." + assert str(e.value) == 'Position 7 is not at a locus boundary.' with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=20, offset=0, region='')) - assert str(e.value) == "Position 20 exceeds multi locus length." + assert str(e.value) == 'Position 20 exceeds multi locus length.' def test_transcribed_invalid_offset(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=-5, region='')) - assert str(e.value) == "Offset -5 at the first exon should be in the upstream region." + assert str(e.value) == 'Offset -5 at the first exon should be in the upstream region.' with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=0, offset=1, region='')) - assert str(e.value) == "Offset 1 should be at a locus end." + assert str(e.value) == 'Offset 1 should be at a locus end.' with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=0, offset=10, region='')) - assert str(e.value) == "Offset 10 exceeds intron length." + assert str(e.value) == 'Offset 10 exceeds intron length.' with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=4, offset=6, region='')) - assert str(e.value) == "Offset 6 exceeds intron length." + assert str(e.value) == 'Offset 6 exceeds intron length.' with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=4, offset=-1, region='')) - assert str(e.value) == "Offset -1 should be at a locus start." + assert str(e.value) == 'Offset -1 should be at a locus start.' with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=5, offset=2, region='')) - assert str(e.value) == "Offset 2 should be at a locus end." + assert str(e.value) == 'Offset 2 should be at a locus end.' with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=5, offset=-6, region='')) - assert str(e.value) == "Offset -6 exceeds intron length." + assert str(e.value) == 'Offset -6 exceeds intron length.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=1, region='')) - assert str(e.value) == "Offset 1 at the last exon should be in the downstream region." + assert str(e.value) == 'Offset 1 at the last exon should be in the downstream region.' def test_transcribed_invalid_offset_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=5, region='')) - assert str(e.value) == "Offset 5 at the first exon on the reverse complement should be in the downstream region." + assert str(e.value) == 'Offset 5 at the first exon on the reverse complement should be in the downstream region.' with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=9, offset=-1, region='')) - assert str(e.value) == "Offset -1 should be at a locus start." + assert str(e.value) == 'Offset -1 should be at a locus start.' with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=9, offset=-10, region='')) - assert str(e.value) == "Offset -10 exceeds intron length." + assert str(e.value) == 'Offset -10 exceeds intron length.' with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=5, offset=-6, region='')) - assert str(e.value) == "Offset -6 exceeds intron length." + assert str(e.value) == 'Offset -6 exceeds intron length.' with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=5, offset=1, region='')) - assert str(e.value) == "Offset 1 should be at a locus end." + assert str(e.value) == 'Offset 1 should be at a locus end.' with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=4, offset=6, region='')) - assert str(e.value) == "Offset 6 exceeds intron length." + assert str(e.value) == 'Offset 6 exceeds intron length.' with pytest.raises(IndexError) as e: multi_locus.to_coordinate(Point(position=4, offset=-1, region='')) - assert str(e.value) == "Offset -1 should be at a locus start." + assert str(e.value) == 'Offset -1 should be at a locus start.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=-5, region='')) - assert str(e.value) == "Offset -5 at the first exon on the reverse complement should be in the upstream region." + assert str(e.value) == 'Offset -5 at the first exon on the reverse complement should be in the upstream region.' def test_downstream_invalid_position(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=1, region='d')) - assert str(e.value) == "Position 0 is not at downstream boundary." + assert str(e.value) == 'Position 0 is not at downstream boundary.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=-1, offset=1, region='d')) - assert str(e.value) == "Value must be non-negative." + assert str(e.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=11, offset=1, region='d')) - assert str(e.value) == "Position 11 is not at downstream boundary." + assert str(e.value) == 'Position 11 is not at downstream boundary.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=100, offset=1, region='d')) - assert str(e.value) == "Position 100 is not at downstream boundary." + assert str(e.value) == 'Position 100 is not at downstream boundary.' def test_downstream_invalid_position_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=0, offset=1, region='d')) - assert str(e.value) == "Position 0 is not at downstream boundary." + assert str(e.value) == 'Position 0 is not at downstream boundary.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=-1, offset=1, region='d')) - assert str(e.value) == "Value must be non-negative." + assert str(e.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=11, offset=1, region='d')) - assert str(e.value) == "Position 11 is not at downstream boundary." + assert str(e.value) == 'Position 11 is not at downstream boundary.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=100, offset=1, region='d')) - assert str(e.value) == "Position 100 is not at downstream boundary." + assert str(e.value) == 'Position 100 is not at downstream boundary.' def test_downstream_invalid_offset(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=-1, region='d')) - assert str(e.value) == "Offset -1 at downstream boundary should be positive." + assert str(e.value) == 'Offset -1 at downstream boundary should be positive.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=0, region='d')) - assert str(e.value) == "Offset 0 at downstream boundary should be positive." + assert str(e.value) == 'Offset 0 at downstream boundary should be positive.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=-5, region='d')) - assert str(e.value) == "Offset -5 at downstream boundary should be positive." + assert str(e.value) == 'Offset -5 at downstream boundary should be positive.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=6, region='d')) - assert str(e.value) == "Offset 6 exceeds downstream region." + assert str(e.value) == 'Offset 6 exceeds downstream region.' def test_downstream_invalid_offset_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=-1, region='d')) - assert str(e.value) == "Offset -1 at downstream boundary should be positive." + assert str(e.value) == 'Offset -1 at downstream boundary should be positive.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=0, region='d')) - assert str(e.value) == "Offset 0 at downstream boundary should be positive." + assert str(e.value) == 'Offset 0 at downstream boundary should be positive.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=-5, region='d')) - assert str(e.value) == "Offset -5 at downstream boundary should be positive." + assert str(e.value) == 'Offset -5 at downstream boundary should be positive.' with pytest.raises(ValueError) as e: multi_locus.to_coordinate(Point(position=9, offset=6, region='d')) - assert str(e.value) == "Offset 6 exceeds downstream region." + assert str(e.value) == 'Offset 6 exceeds downstream region.' From 68c57182ee6cb5d66cb25904dcf728400304bb70 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 31 Aug 2026 15:22:42 +0200 Subject: [PATCH 200/236] Style according to PEP8. --- mutalyzer_crossmapper/locus.py | 27 ++++++++++++--------------- tests/test_locus.py | 2 ++ 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 4103dfe..d05987d 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -22,20 +22,20 @@ def __post_init__(self) -> None: def _check_int(value: int) -> None: - """Check if the value type is integer.""" + """Check if the input value type is integer.""" if not isinstance(value, int): raise ValueError('Value must be an integer.') def _check_non_negative_int(value: int) -> None: - """Check if the coordinate is a non-negative integer.""" + """Check if the value is a non-negative integer.""" _check_int(value) if value < 0: raise ValueError('Value must be non-negative.') def _check_locus(locus: tuple[int, int]) -> None: - """Check if the range is valid.""" + """Check if the locus location is valid.""" if not isinstance(locus, tuple) or len(locus) != 2: raise ValueError('Locus must be a tuple of two values.') @@ -50,7 +50,8 @@ class Locus(object): """Locus object.""" def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: - """ + """Initialize a Locus object. + :arg tuple location: Locus location. :arg bool inverted: Orientation. """ @@ -58,21 +59,17 @@ def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: self._inverted = inverted self.boundary = location[0], location[1] - 1 - self._end = location[1] - location[0] # one-based length of the locus + self._end = location[1] - location[0] # one-based length of the locus def _validate_point(self, position: int, offset: int) -> None: - """Validate a locus Point dataclass according to HGVS rules. - - :arg int position: Position. - :arg int offset: Offset. - """ - if offset != 0 and position not in (0, self._end-1): + """Validate a locus Point dataclass according to HGVS rules.""" + if offset != 0 and position not in (0, self._end - 1): raise ValueError(f'Position {position} is not at a locus boundary.') if offset < 0 and position != 0: raise IndexError(f'Offset {offset} should be at a locus start.') - if offset > 0 and position != self._end-1: + if offset > 0 and position != self._end - 1: raise IndexError(f'Offset {offset} should be at a locus end.') - if position > self._end-1: + if position > self._end - 1: raise IndexError(f'Position {position} exceeds locus length.') def to_position(self, coord: Coord) -> Point: @@ -86,13 +83,13 @@ def to_position(self, coord: Coord) -> Point: if coord.coordinate > self.boundary[1]: return Point(position=0, offset=self.boundary[1] - coord.coordinate) if coord.coordinate < self.boundary[0]: - return Point(position=self._end-1, offset=self.boundary[0] - coord.coordinate) + return Point(position=self._end - 1, offset=self.boundary[0] - coord.coordinate) return Point(position=self.boundary[1] - coord.coordinate, offset=0) if coord.coordinate < self.boundary[0]: return Point(position=0, offset=coord.coordinate - self.boundary[0]) if coord.coordinate > self.boundary[1]: - return Point(position=self._end-1, offset=coord.coordinate - self.boundary[1]) + return Point(position=self._end - 1, offset=coord.coordinate - self.boundary[1]) return Point(position=coord.coordinate - self.boundary[0], offset=0) def to_coordinate(self, point: Point) -> Coord: diff --git a/tests/test_locus.py b/tests/test_locus.py index efae896..3a41861 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -74,6 +74,7 @@ def test_invalid_Coord_initialization(): def test_invalid_Locus_point(): """Forward orientent Locus with invalid point.""" locus = Locus((30, 35)) + with pytest.raises(ValueError) as e: locus.to_coordinate(Point(position=-5, offset=0)) assert str(e.value) == 'Value must be non-negative.' @@ -95,6 +96,7 @@ def test_invalid_Locus_point(): def test_invalid_Locus_inverted_point(): """Reverse orientent Locus with invalid point.""" locus = Locus((30, 35), True) + with pytest.raises(ValueError) as e: locus.to_coordinate(Point(position=-5, offset=0)) assert str(e.value) == 'Value must be non-negative.' From f84410e74a2caedf12dee280ad37936679f95257 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Mon, 31 Aug 2026 15:30:16 +0200 Subject: [PATCH 201/236] Style according to pylint. --- tests/test_locus.py | 128 ++++++++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/tests/test_locus.py b/tests/test_locus.py index 3a41861..5667212 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -4,117 +4,117 @@ from mutalyzer_crossmapper.locus import Coord, Locus, Point -def test_invalid_Locus_initialization(): +def test_invalid_locus_initialization(): """Test Locus initialization.""" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: Locus((10, 5)) - assert str(e.value) == 'Locus start 10 must be smaller than locus end 5.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus start 10 must be smaller than locus end 5.' + with pytest.raises(ValueError) as error: Locus((10, 20, 30)) - assert str(e.value) == 'Locus must be a tuple of two values.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus must be a tuple of two values.' + with pytest.raises(ValueError) as error: Locus((10, -5)) - assert str(e.value) == 'Value must be non-negative.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be non-negative.' + with pytest.raises(ValueError) as error: Locus((10, 20.5)) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: Locus((10, None)) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: Locus(('10', '20')) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: Locus((10, 10)) - assert str(e.value) == 'Locus start 10 must be smaller than locus end 10.' + assert str(error.value) == 'Locus start 10 must be smaller than locus end 10.' # Inverted Locus initialization - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: Locus((10, 5), inverted=True) - assert str(e.value) == 'Locus start 10 must be smaller than locus end 5.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus start 10 must be smaller than locus end 5.' + with pytest.raises(ValueError) as error: Locus((10, 20, 30), inverted=True) - assert str(e.value) == 'Locus must be a tuple of two values.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus must be a tuple of two values.' + with pytest.raises(ValueError) as error: Locus((10, -5), inverted=True) - assert str(e.value) == 'Value must be non-negative.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be non-negative.' + with pytest.raises(ValueError) as error: Locus((10, 20.5), inverted=True) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: Locus((10, None), inverted=True) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: Locus(('10', '20'), inverted=True) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: Locus((10, 10), inverted=True) - assert str(e.value) == 'Locus start 10 must be smaller than locus end 10.' + assert str(error.value) == 'Locus start 10 must be smaller than locus end 10.' -def test_invalid_Coord_initialization(): +def test_invalid_coord_initialization(): """Test Coord initialization.""" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: Coord(-1) - assert str(e.value) == 'Value must be non-negative.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be non-negative.' + with pytest.raises(ValueError) as error: Coord(3.5) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: Coord('10') - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: Coord(None) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: Coord([10]) - assert str(e.value) == 'Value must be an integer.' + assert str(error.value) == 'Value must be an integer.' -def test_invalid_Locus_point(): +def test_invalid_locus_point(): """Forward orientent Locus with invalid point.""" locus = Locus((30, 35)) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: locus.to_coordinate(Point(position=-5, offset=0)) - assert str(e.value) == 'Value must be non-negative.' + assert str(error.value) == 'Value must be non-negative.' - with pytest.raises(IndexError) as e: + with pytest.raises(IndexError) as error: locus.to_coordinate(Point(position=5, offset=0)) - assert str(e.value) == 'Position 5 exceeds locus length.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Position 5 exceeds locus length.' + with pytest.raises(IndexError) as error: locus.to_coordinate(Point(position=0, offset=2)) - assert str(e.value) == 'Offset 2 should be at a locus end.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset 2 should be at a locus end.' + with pytest.raises(IndexError) as error: locus.to_coordinate(Point(position=4, offset=-2)) - assert str(e.value) == 'Offset -2 should be at a locus start.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset -2 should be at a locus start.' + with pytest.raises(ValueError) as error: locus.to_coordinate(Point(position=2, offset=1)) - assert str(e.value) == 'Position 2 is not at a locus boundary.' + assert str(error.value) == 'Position 2 is not at a locus boundary.' -def test_invalid_Locus_inverted_point(): +def test_invalid_locus_inverted_point(): """Reverse orientent Locus with invalid point.""" locus = Locus((30, 35), True) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: locus.to_coordinate(Point(position=-5, offset=0)) - assert str(e.value) == 'Value must be non-negative.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Value must be non-negative.' + with pytest.raises(IndexError) as error: locus.to_coordinate(Point(position=5, offset=0)) - assert str(e.value) == 'Position 5 exceeds locus length.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Position 5 exceeds locus length.' + with pytest.raises(IndexError) as error: locus.to_coordinate(Point(position=0, offset=2)) - assert str(e.value) == 'Offset 2 should be at a locus end.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset 2 should be at a locus end.' + with pytest.raises(IndexError) as error: locus.to_coordinate(Point(position=4, offset=-2)) - assert str(e.value) == 'Offset -2 should be at a locus start.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset -2 should be at a locus start.' + with pytest.raises(ValueError) as error: locus.to_coordinate(Point(position=2, offset=1)) - assert str(e.value) == 'Position 2 is not at a locus boundary.' + assert str(error.value) == 'Position 2 is not at a locus boundary.' -def test_Locus(): +def test_locus(): """Forward orientent Locus.""" locus = Locus((30, 35)) @@ -126,7 +126,7 @@ def test_Locus(): invariant(locus.to_position, Coord(35), locus.to_coordinate, Point(position=4, offset=1)) -def test_Locus_inverted(): +def test_locus_inverted(): """Reverse orientent Locus.""" locus = Locus((30, 35), True) From 85a5003ca90ec9a0977b6daf3eb7aafd5bcde00d Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 1 Sep 2026 01:27:53 +0200 Subject: [PATCH 202/236] Style according to pylint. --- mutalyzer_crossmapper/locus.py | 4 +- mutalyzer_crossmapper/multi_locus.py | 221 +++++++++++--------- tests/test_multi_locus.py | 300 +++++++++++++-------------- 3 files changed, 276 insertions(+), 249 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index d05987d..d25c4ad 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -62,7 +62,7 @@ def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: self._end = location[1] - location[0] # one-based length of the locus def _validate_point(self, position: int, offset: int) -> None: - """Validate a locus Point dataclass according to HGVS rules.""" + """Validate a locus point dataclass according to HGVS rules.""" if offset != 0 and position not in (0, self._end - 1): raise ValueError(f'Position {position} is not at a locus boundary.') if offset < 0 and position != 0: @@ -93,7 +93,7 @@ def to_position(self, coord: Coord) -> Point: return Point(position=coord.coordinate - self.boundary[0], offset=0) def to_coordinate(self, point: Point) -> Coord: - """Convert a locus dataclass point model to a coordinate dataclass. + """Convert a locus point dataclass to a coordinate dataclass. :arg Point point: Locus point dataclass. diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 5709252..9b60743 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -2,7 +2,6 @@ from itertools import accumulate from dataclasses import dataclass - from .location import _nearest_location from .locus import Locus, Coord, _check_locus from .locus import Point as LocusPoint @@ -11,32 +10,39 @@ @dataclass(slots=True) class Point(LocusPoint): """Point dataclass""" - region: str = '' + + region: str = "" def __post_init__(self) -> None: LocusPoint.__post_init__(self) - if self.region not in ('', 'u', 'd'): - raise ValueError(f'Region {self.region} is not valid. Must be "", "u", or "d".') + if self.region not in ("", "u", "d"): + raise ValueError( + f"Region {self.region} is invalid, it must be a string from '', 'u' or 'd'." + ) -def _check_in_range(value: int, length: int | None = None) -> None: +def _check_in_range(value: int, length: int) -> None: """Check if the value no larger than length.""" - if length is not None and value >= length: - raise ValueError(f'Value {value} must be within the bounds of the reference length {length}.') + if value >= length: + raise ValueError( + f"Location {value} must be within the bounds of the reference length {length}." + ) def _check_multi_locus(locations: list[tuple[int, int]], length: int | None = None) -> None: """Check if the locations list is valid.""" if not locations or not isinstance(locations, list): - raise ValueError('Locations must be a non-empty list of tuples.') + raise ValueError("Locations must be a non-empty list of tuples.") + for locus in locations: _check_locus(locus) for l1, l2 in zip(locations, locations[1:]): if l2[0] < l1[1]: - raise ValueError(f'Locus {l2} and locus {l1} are overlapping.') + raise ValueError(f"Locus {l2} and locus {l1} are overlapping.") - _check_in_range(locations[-1][1], length) + if length is not None: + _check_in_range(locations[-1][1], length) def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: @@ -47,15 +53,21 @@ def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: :returns list: List of cumulative location lengths. """ - return [0] + list(accumulate(map( - lambda x: x[1] - x[0], locations[::orientation][:-1]))) + return [0] + list(accumulate(map(lambda x: x[1] - x[0], locations[::orientation][:-1]))) class MultiLocus(object): """MultiLocus object.""" - def __init__(self, locations: list[tuple[int, int]], length: int |None = None, inverted: bool = False) -> None: + + def __init__( + self, + locations: list[tuple[int, int]], + length: int | None = None, + inverted: bool = False, + ) -> None: """ :arg list locations: List of locus locations. + :arg int|None length: Length of the reference sequence, None if unknown. :arg bool inverted: Orientation. """ _check_multi_locus(locations, length) @@ -66,90 +78,104 @@ def __init__(self, locations: list[tuple[int, int]], length: int |None = None, i self._loci = [Locus(location, inverted) for location in locations] self._orientation = -1 if inverted else 1 self._offsets = _offsets(locations, self._orientation) - self._end = sum(end - start for start, end in locations) # one-based length of the MultiLocus + # one-based length of the MultiLocus + self._end = sum(end - start for start, end in locations) - def _validate_coord(self, coordinate:int) -> None: + def _validate_coord(self, coordinate: int) -> None: """Check if the coordinate is valid.""" if self._length is not None: _check_in_range(coordinate, self._length) def _validate_point(self, index: int, position: int, offset: int, region: str) -> None: - """Check if a point is valid under HGVS rules. + """Validate if a multi locus Point is valid under HGVS rules. :arg int index: Index of the locus. :arg int position: Position. :arg int offset: Offset. :arg str region: Region. """ - # Upstream region validation, position is constant value and offset should not be positive - if region == 'u': - if position != self._offsets[0]: - raise ValueError(f'Position {position} is not at upstream boundary.') + if region == "u": + if position != 0: + raise ValueError(f"Position {position} is not at upstream boundary.") if offset >= 0: - raise ValueError(f'Offset {offset} at upstream boundary should be negative.') + raise ValueError( + f"Offset {offset} at upstream boundary should be negative." + ) if self._inverted: - if self._length is not None and -offset >= self._length - self._loci[self._direction(0)].boundary[1]: - raise ValueError(f'Offset {offset} exceeds upstream region.') + if ( + self._length is not None + and -offset + >= self._length - self._loci[self._direction(0)].boundary[1] + ): + raise ValueError(f"Offset {offset} exceeds upstream region.") else: if -offset > self._loci[self._direction(0)].boundary[0]: - raise ValueError(f'Offset {offset} exceeds upstream region.') + raise ValueError(f"Offset {offset} exceeds upstream region.") - # Downstream region validation, position is constant value and offset should not be negative - if region == 'd': - if position != self._end-1: - raise ValueError(f'Position {position} is not at downstream boundary.') + if region == "d": + if position != self._end - 1: + raise ValueError(f"Position {position} is not at downstream boundary.") if offset <= 0: - raise ValueError(f'Offset {offset} at downstream boundary should be positive.') + raise ValueError( + f"Offset {offset} at downstream boundary should be positive." + ) if not self._inverted: - if self._length is not None and offset >= self._length - self._loci[self._direction(-1)].boundary[1]: - raise ValueError(f'Offset {offset} exceeds downstream region.') + if ( + self._length is not None + and offset + >= self._length - self._loci[self._direction(-1)].boundary[1] + ): + raise ValueError(f"Offset {offset} exceeds downstream region.") else: - if offset > self._loci[self._direction(len(self._locations)-1)].boundary[0]: - raise ValueError(f'Offset {offset} exceeds downstream region.') - - # '' region validation, position should be within the MultiLocus and offset should not exceed intron length - if region == '': - if self._inverted: - if offset < 0: - if self._direction(index) == len(self._loci) - 1: - if position == 0: - raise ValueError( - f'Offset {offset} at the first exon on the reverse complement should be in the upstream region.' - ) - else: - if -offset >= self._loci[self._direction(index-1)].boundary[0] - self._loci[self._direction(index)].boundary[1]: - raise IndexError(f'Offset {offset} exceeds intron length.') - if offset > 0: - if self._direction(index) == 0: - if position == self._end-1: - raise ValueError( - f'Offset {offset} at the first exon on the reverse complement should be in the downstream region.' - ) - else: - if offset >= self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index+1)].boundary[1]: - raise IndexError(f'Offset {offset} exceeds intron length.') - - - if not self._inverted: - if offset < 0: - if self._direction(index) == 0: - if position == 0: - raise ValueError( - f'Offset {offset} at the first exon should be in the upstream region.' - ) - else: - if -offset >= self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index-1)].boundary[1]: - raise IndexError(f'Offset {offset} exceeds intron length.') - if offset > 0: - if self._direction(index) == len(self._loci) - 1: - if position == self._end-1: - raise ValueError( - f'Offset {offset} at the last exon should be in the downstream region.' - ) - else: - if offset >= self._loci[self._direction(index+1)].boundary[0] - self._loci[self._direction(index)].boundary[1]: - raise IndexError(f'Offset {offset} exceeds intron length.') - + if ( + offset + > self._loci[self._direction(len(self._locations) - 1)].boundary[0] + ): + raise ValueError(f"Offset {offset} exceeds downstream region.") + + if region == "": + if offset < 0: + if index == 0 and position == 0: + raise ValueError( + f"Offset {offset} at the first locus should be in the upstream region." + ) + if self._inverted: + if ( + self._direction(index) != len(self._loci) - 1 + and -offset + >= self._loci[self._direction(index - 1)].boundary[0] + - self._loci[self._direction(index)].boundary[1] + ): + raise IndexError(f"Offset {offset} exceeds intron length.") + else: + if ( + self._direction(index) != 0 + and -offset + >= self._loci[self._direction(index)].boundary[0] + - self._loci[self._direction(index - 1)].boundary[1] + ): + raise IndexError(f"Offset {offset} exceeds intron length.") + if offset > 0: + if index == len(self._loci) - 1 and position == self._end - 1: + raise ValueError( + f"Offset {offset} at the last locus should be in the downstream region." + ) + if self._inverted: + if ( + self._direction(index) != 0 + and offset + >= self._loci[self._direction(index)].boundary[0] + - self._loci[self._direction(index + 1)].boundary[1] + ): + raise IndexError(f"Offset {offset} exceeds intron length.") + else: + if ( + self._direction(index) != len(self._loci) - 1 + and offset + >= self._loci[self._direction(index + 1)].boundary[0] + - self._loci[self._direction(index)].boundary[1] + ): + raise IndexError(f"Offset {offset} exceeds intron length.") def _direction(self, index: int) -> int: if self._inverted: @@ -170,16 +196,16 @@ def _outside(self, coordinate: int) -> int: return 0 def to_position(self, coord: Coord) -> Point: - """Convert a coordinate to a point model. + """Convert a coordinate dataclass to a multi locus point dataclass. - :arg Coord coord: Coordinate model. + :arg Coord coord: Coordinate dataclass. - :returns Point: Point model. + :returns Point: Multi locus point dataclass. """ self._validate_coord(coord.coordinate) index = _nearest_location(self._locations, coord.coordinate, self._inverted) outside = self._orientation * self._outside(coord.coordinate) - region = 'u' if outside < 0 else 'd' if outside > 0 else '' + region = "u" if outside < 0 else "d" if outside > 0 else "" point = self._loci[index].to_position(coord) return Point( @@ -189,23 +215,22 @@ def to_position(self, coord: Coord) -> Point: ) def to_coordinate(self, point: Point) -> Coord: - """Convert a point model to a coordinate. + """Convert a multi locus point dataclass to a coordinate dataclass. - :arg Point point: Point model. + :arg Point point: Multi locus point dataclass. - :returns Coord: Coordinate module. + :returns Coord: Coordinate dataclass. """ index = min( - len(self._offsets), - max(0, bisect_right(self._offsets, point.position) - 1) + len(self._offsets), max(0, bisect_right(self._offsets, point.position) - 1) ) self._validate_point(index, point.position, point.offset, point.region) - if point.region == 'u': + if point.region == "u": if self._inverted: return Coord(self._locations[-1][1] - point.offset - 1) return Coord(self._locations[0][0] + point.offset) - if point.region == 'd': + if point.region == "d": if self._inverted: return Coord(self._locations[0][0] - point.offset) return Coord(self._locations[-1][1] + point.offset - 1) @@ -218,14 +243,16 @@ def to_coordinate(self, point: Point) -> Coord: ) ) - except ValueError as e: - if 'Position' in str(e): - raise ValueError(str(e).replace(str(point.position - self._offsets[index]), str(point.position))) from e - raise - except IndexError as e: - if 'Position' in str(e): + except ValueError as error: + if "Position" in str(error): + raise ValueError( + str(error).replace( + str(point.position - self._offsets[index]), str(point.position) + ) + ) from error + except IndexError as error: + if "Position" in str(error): raise IndexError( - f'Position {point.position} exceeds multi locus length.' - ) from e + f"Position {point.position} exceeds multi locus length." + ) from error raise - diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index dce3956..325e3e5 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -30,86 +30,86 @@ def test_offsets_adjacent_inverted(): ## Test MultiLocus model and its point model def test_invalid_MultiLocus_initialization(): """Test MultiLocus initialization.""" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: MultiLocus(([(10, 5), (20, 25)])) - assert str(e.value) == 'Locus start 10 must be smaller than locus end 5.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus start 10 must be smaller than locus end 5.' + with pytest.raises(ValueError) as error: MultiLocus([(10, 20, 30), (40, 50)]) - assert str(e.value) == 'Locus must be a tuple of two values.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus must be a tuple of two values.' + with pytest.raises(ValueError) as error: MultiLocus([(10, -5), (20, 25)]) - assert str(e.value) == 'Value must be non-negative.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be non-negative.' + with pytest.raises(ValueError) as error: MultiLocus([(10, 20.5), (30, 40)]) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: MultiLocus([(10.5, None), (20, 30)]) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: MultiLocus([('10', '20'), (30, 40)]) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: MultiLocus([(10, 20), (15, 25)]) - assert str(e.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' + with pytest.raises(ValueError) as error: MultiLocus([(10, 12), (15, 25)], 25) - assert str(e.value) == 'Value 25 must be within the bounds of the reference length 25.' + assert str(error.value) == 'Location 25 must be within the bounds of the reference length 25.' # Inverted MultiLocus initialization - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: MultiLocus(([(10, 5), (20, 25)]), inverted=True) - assert str(e.value) == 'Locus start 10 must be smaller than locus end 5.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus start 10 must be smaller than locus end 5.' + with pytest.raises(ValueError) as error: MultiLocus([(10, 20, 30), (40, 50)], inverted=True) - assert str(e.value) == 'Locus must be a tuple of two values.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus must be a tuple of two values.' + with pytest.raises(ValueError) as error: MultiLocus([(10, -5), (20, 25)], inverted=True) - assert str(e.value) == 'Value must be non-negative.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be non-negative.' + with pytest.raises(ValueError) as error: MultiLocus([(10, 20.5), (30, 40)], inverted=True) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: MultiLocus([(10.5, None), (20, 30)], inverted=True) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: MultiLocus([('10', '20'), (30, 40)], inverted=True) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: MultiLocus([(10, 20), (15, 25)], 25, inverted=True) - assert str(e.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' + with pytest.raises(ValueError) as error: MultiLocus([(10, 12), (15, 25)], 25, inverted=True) - assert str(e.value) == 'Value 25 must be within the bounds of the reference length 25.' + assert str(error.value) == 'Location 25 must be within the bounds of the reference length 25.' def test_MultiLocus_invalid_coordinate(): """Forward orientent MultiLocus with invalid coordinate.""" multi_locus = MultiLocus([(30, 35), (40, 45)]) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_position(Coord(-1)) - assert str(e.value) == 'Value must be non-negative.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be non-negative.' + with pytest.raises(ValueError) as error: multi_locus.to_position(Coord(46.7)) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: multi_locus.to_position(Coord('31')) - assert str(e.value) == 'Value must be an integer.' + assert str(error.value) == 'Value must be an integer.' def test_invalid_Point_initialization(): """Test Point initialization.""" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: Point(position=-1, offset=0, region='') - assert str(e.value) == 'Value must be non-negative.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be non-negative.' + with pytest.raises(ValueError) as error: Point(position=0, offset=0, region='*') - assert str(e.value) == 'Region * is not valid. Must be "", "u", or "d".' - with pytest.raises(ValueError) as e: + assert str(error.value) == "Region * is invalid, it must be a string from '', 'u' or 'd'." + with pytest.raises(ValueError) as error: Point(position=0, offset=0, region=None) - assert str(e.value) == 'Region None is not valid. Must be "", "u", or "d".' - with pytest.raises(ValueError) as e: + assert str(error.value) == "Region None is invalid, it must be a string from '', 'u' or 'd'." + with pytest.raises(ValueError) as error: Point(position='11', offset=0, region='u') - assert str(e.value) == 'Value must be an integer.' + assert str(error.value) == 'Value must be an integer.' def test_MultiLocus(): @@ -279,9 +279,9 @@ def test_MultiLocus_with_length(): Point(position=21, offset=2, region='d'), ) # Boundary between the last base and beyond the last base. - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_position(Coord(74)) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=21, offset=3, region='d')) @@ -309,9 +309,9 @@ def test_MultiLocus_inverted_with_length(): multi_locus.to_coordinate, Point(position=0, offset=-2, region='u'), ) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_position(Coord(74)) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=-3, region='u')) @@ -506,198 +506,198 @@ def test_one_base_exon_inverted(): def test_upstream_invalid_position(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=1, offset=-1, region='u')) - assert str(e.value) == 'Position 1 is not at upstream boundary.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 1 is not at upstream boundary.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=-1, offset=-1, region='u')) - assert str(e.value) == 'Value must be non-negative.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be non-negative.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=20, offset=-1, region='u')) - assert str(e.value) == 'Position 20 is not at upstream boundary.' + assert str(error.value) == 'Position 20 is not at upstream boundary.' def test_upstream_invalid_position_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=1, offset=-1, region='u')) - assert str(e.value) == 'Position 1 is not at upstream boundary.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 1 is not at upstream boundary.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=-1, offset=-1, region='u')) - assert str(e.value) == 'Value must be non-negative.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be non-negative.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=20, offset=-1, region='u')) - assert str(e.value) == 'Position 20 is not at upstream boundary.' + assert str(error.value) == 'Position 20 is not at upstream boundary.' def test_upstream_invalid_offset(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=1, region='u')) - assert str(e.value) == 'Offset 1 at upstream boundary should be negative.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset 1 at upstream boundary should be negative.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=-6, region='u')) - assert str(e.value) == 'Offset -6 exceeds upstream region.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset -6 exceeds upstream region.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=0, region='u')) - assert str(e.value) == 'Offset 0 at upstream boundary should be negative.' + assert str(error.value) == 'Offset 0 at upstream boundary should be negative.' def test_upstream_invalid_offset_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=1, region='u')) - assert str(e.value) == 'Offset 1 at upstream boundary should be negative.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset 1 at upstream boundary should be negative.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=-6, region='u')) - assert str(e.value) == 'Offset -6 exceeds upstream region.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset -6 exceeds upstream region.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=0, region='u')) - assert str(e.value) == 'Offset 0 at upstream boundary should be negative.' + assert str(error.value) == 'Offset 0 at upstream boundary should be negative.' def test_transcribed_invalid_position(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=-1, offset=0, region='')) - assert str(e.value) == 'Value must be non-negative.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be non-negative.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=7, offset=1, region='')) - assert str(e.value) == 'Position 7 is not at a locus boundary.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Position 7 is not at a locus boundary.' + with pytest.raises(IndexError) as error: multi_locus.to_coordinate(Point(position=20, offset=0, region='')) - assert str(e.value) == 'Position 20 exceeds multi locus length.' + assert str(error.value) == 'Position 20 exceeds multi locus length.' def test_transcribed_invalid_position_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=-1, offset=0, region='')) - assert str(e.value) == 'Value must be non-negative.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be non-negative.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=7, offset=-1, region='')) - assert str(e.value) == 'Position 7 is not at a locus boundary.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Position 7 is not at a locus boundary.' + with pytest.raises(IndexError) as error: multi_locus.to_coordinate(Point(position=20, offset=0, region='')) - assert str(e.value) == 'Position 20 exceeds multi locus length.' + assert str(error.value) == 'Position 20 exceeds multi locus length.' def test_transcribed_invalid_offset(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=-5, region='')) - assert str(e.value) == 'Offset -5 at the first exon should be in the upstream region.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset -5 at the first locus should be in the upstream region.' + with pytest.raises(IndexError) as error: multi_locus.to_coordinate(Point(position=0, offset=1, region='')) - assert str(e.value) == 'Offset 1 should be at a locus end.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset 1 should be at a locus end.' + with pytest.raises(IndexError) as error: multi_locus.to_coordinate(Point(position=0, offset=10, region='')) - assert str(e.value) == 'Offset 10 exceeds intron length.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset 10 exceeds intron length.' + with pytest.raises(IndexError) as error: multi_locus.to_coordinate(Point(position=4, offset=6, region='')) - assert str(e.value) == 'Offset 6 exceeds intron length.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset 6 exceeds intron length.' + with pytest.raises(IndexError) as error: multi_locus.to_coordinate(Point(position=4, offset=-1, region='')) - assert str(e.value) == 'Offset -1 should be at a locus start.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset -1 should be at a locus start.' + with pytest.raises(IndexError) as error: multi_locus.to_coordinate(Point(position=5, offset=2, region='')) - assert str(e.value) == 'Offset 2 should be at a locus end.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset 2 should be at a locus end.' + with pytest.raises(IndexError) as error: multi_locus.to_coordinate(Point(position=5, offset=-6, region='')) - assert str(e.value) == 'Offset -6 exceeds intron length.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset -6 exceeds intron length.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=1, region='')) - assert str(e.value) == 'Offset 1 at the last exon should be in the downstream region.' + assert str(error.value) == 'Offset 1 at the last locus should be in the downstream region.' def test_transcribed_invalid_offset_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=5, region='')) - assert str(e.value) == 'Offset 5 at the first exon on the reverse complement should be in the downstream region.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset 5 at the last locus should be in the downstream region.' + with pytest.raises(IndexError) as error: multi_locus.to_coordinate(Point(position=9, offset=-1, region='')) - assert str(e.value) == 'Offset -1 should be at a locus start.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset -1 should be at a locus start.' + with pytest.raises(IndexError) as error: multi_locus.to_coordinate(Point(position=9, offset=-10, region='')) - assert str(e.value) == 'Offset -10 exceeds intron length.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset -10 exceeds intron length.' + with pytest.raises(IndexError) as error: multi_locus.to_coordinate(Point(position=5, offset=-6, region='')) - assert str(e.value) == 'Offset -6 exceeds intron length.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset -6 exceeds intron length.' + with pytest.raises(IndexError) as error: multi_locus.to_coordinate(Point(position=5, offset=1, region='')) - assert str(e.value) == 'Offset 1 should be at a locus end.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset 1 should be at a locus end.' + with pytest.raises(IndexError) as error: multi_locus.to_coordinate(Point(position=4, offset=6, region='')) - assert str(e.value) == 'Offset 6 exceeds intron length.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset 6 exceeds intron length.' + with pytest.raises(IndexError) as error: multi_locus.to_coordinate(Point(position=4, offset=-1, region='')) - assert str(e.value) == 'Offset -1 should be at a locus start.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset -1 should be at a locus start.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=-5, region='')) - assert str(e.value) == 'Offset -5 at the first exon on the reverse complement should be in the upstream region.' + assert str(error.value) == 'Offset -5 at the first locus should be in the upstream region.' def test_downstream_invalid_position(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=1, region='d')) - assert str(e.value) == 'Position 0 is not at downstream boundary.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 is not at downstream boundary.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=-1, offset=1, region='d')) - assert str(e.value) == 'Value must be non-negative.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be non-negative.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=11, offset=1, region='d')) - assert str(e.value) == 'Position 11 is not at downstream boundary.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 11 is not at downstream boundary.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=100, offset=1, region='d')) - assert str(e.value) == 'Position 100 is not at downstream boundary.' + assert str(error.value) == 'Position 100 is not at downstream boundary.' def test_downstream_invalid_position_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=1, region='d')) - assert str(e.value) == 'Position 0 is not at downstream boundary.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 is not at downstream boundary.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=-1, offset=1, region='d')) - assert str(e.value) == 'Value must be non-negative.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be non-negative.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=11, offset=1, region='d')) - assert str(e.value) == 'Position 11 is not at downstream boundary.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 11 is not at downstream boundary.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=100, offset=1, region='d')) - assert str(e.value) == 'Position 100 is not at downstream boundary.' + assert str(error.value) == 'Position 100 is not at downstream boundary.' def test_downstream_invalid_offset(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=-1, region='d')) - assert str(e.value) == 'Offset -1 at downstream boundary should be positive.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset -1 at downstream boundary should be positive.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=0, region='d')) - assert str(e.value) == 'Offset 0 at downstream boundary should be positive.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset 0 at downstream boundary should be positive.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=-5, region='d')) - assert str(e.value) == 'Offset -5 at downstream boundary should be positive.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset -5 at downstream boundary should be positive.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=6, region='d')) - assert str(e.value) == 'Offset 6 exceeds downstream region.' + assert str(error.value) == 'Offset 6 exceeds downstream region.' def test_downstream_invalid_offset_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=-1, region='d')) - assert str(e.value) == 'Offset -1 at downstream boundary should be positive.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset -1 at downstream boundary should be positive.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=0, region='d')) - assert str(e.value) == 'Offset 0 at downstream boundary should be positive.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset 0 at downstream boundary should be positive.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=-5, region='d')) - assert str(e.value) == 'Offset -5 at downstream boundary should be positive.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset -5 at downstream boundary should be positive.' + with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=6, region='d')) - assert str(e.value) == 'Offset 6 exceeds downstream region.' + assert str(error.value) == 'Offset 6 exceeds downstream region.' From 69205d82ed4d6f1029e6e0a7fcbc8afbcd45388f Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 1 Sep 2026 01:32:02 +0200 Subject: [PATCH 203/236] Update error message. --- mutalyzer_crossmapper/multi_locus.py | 16 ++++------------ tests/test_multi_locus.py | 21 ++++++++++----------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 9b60743..52a5134 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -98,9 +98,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - if position != 0: raise ValueError(f"Position {position} is not at upstream boundary.") if offset >= 0: - raise ValueError( - f"Offset {offset} at upstream boundary should be negative." - ) + raise ValueError(f"Offset {offset} at upstream region should be negative.") if self._inverted: if ( self._length is not None @@ -116,9 +114,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - if position != self._end - 1: raise ValueError(f"Position {position} is not at downstream boundary.") if offset <= 0: - raise ValueError( - f"Offset {offset} at downstream boundary should be positive." - ) + raise ValueError(f"Offset {offset} at downstream region should be positive.") if not self._inverted: if ( self._length is not None @@ -136,9 +132,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - if region == "": if offset < 0: if index == 0 and position == 0: - raise ValueError( - f"Offset {offset} at the first locus should be in the upstream region." - ) + raise ValueError(f"Offset {offset} at the first locus should be in the upstream region.") if self._inverted: if ( self._direction(index) != len(self._loci) - 1 @@ -157,9 +151,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - raise IndexError(f"Offset {offset} exceeds intron length.") if offset > 0: if index == len(self._loci) - 1 and position == self._end - 1: - raise ValueError( - f"Offset {offset} at the last locus should be in the downstream region." - ) + raise ValueError(f"Offset {offset} at the last locus should be in the downstream region.") if self._inverted: if ( self._direction(index) != 0 diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index 325e3e5..c3379c7 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -534,26 +534,26 @@ def test_upstream_invalid_offset(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=1, region='u')) - assert str(error.value) == 'Offset 1 at upstream boundary should be negative.' + assert str(error.value) == 'Offset 1 at upstream region should be negative.' with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=-6, region='u')) assert str(error.value) == 'Offset -6 exceeds upstream region.' with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=0, region='u')) - assert str(error.value) == 'Offset 0 at upstream boundary should be negative.' + assert str(error.value) == 'Offset 0 at upstream region should be negative.' def test_upstream_invalid_offset_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=1, region='u')) - assert str(error.value) == 'Offset 1 at upstream boundary should be negative.' + assert str(error.value) == 'Offset 1 at upstream region should be negative.' with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=-6, region='u')) assert str(error.value) == 'Offset -6 exceeds upstream region.' with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=0, region='u')) - assert str(error.value) == 'Offset 0 at upstream boundary should be negative.' + assert str(error.value) == 'Offset 0 at upstream region should be negative.' def test_transcribed_invalid_position(): @@ -674,13 +674,13 @@ def test_downstream_invalid_offset(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25) with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=-1, region='d')) - assert str(error.value) == 'Offset -1 at downstream boundary should be positive.' + assert str(error.value) == 'Offset -1 at downstream region should be positive.' with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=0, region='d')) - assert str(error.value) == 'Offset 0 at downstream boundary should be positive.' + assert str(error.value) == 'Offset 0 at downstream region should be positive.' with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=-5, region='d')) - assert str(error.value) == 'Offset -5 at downstream boundary should be positive.' + assert str(error.value) == 'Offset -5 at downstream region should be positive.' with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=6, region='d')) assert str(error.value) == 'Offset 6 exceeds downstream region.' @@ -690,14 +690,13 @@ def test_downstream_invalid_offset_inverted(): multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=-1, region='d')) - assert str(error.value) == 'Offset -1 at downstream boundary should be positive.' + assert str(error.value) == 'Offset -1 at downstream region should be positive.' with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=0, region='d')) - assert str(error.value) == 'Offset 0 at downstream boundary should be positive.' + assert str(error.value) == 'Offset 0 at downstream region should be positive.' with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=-5, region='d')) - assert str(error.value) == 'Offset -5 at downstream boundary should be positive.' + assert str(error.value) == 'Offset -5 at downstream region should be positive.' with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=6, region='d')) assert str(error.value) == 'Offset 6 exceeds downstream region.' - From 77369975b2feb5071c5770e6a07f77704e0a6182 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 1 Sep 2026 09:53:47 +0200 Subject: [PATCH 204/236] Style according to pylint. --- mutalyzer_crossmapper/crossmapper.py | 148 +++++---- tests/test_crossmapper.py | 481 ++++++++++++++------------- 2 files changed, 329 insertions(+), 300 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 328c1e3..525aa55 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -21,21 +21,23 @@ def __str__(self) -> str: class Genomic(object): """Genomic crossmap object.""" def coordinate_to_genomic(self, coord: Coord, length: int | None = None) -> GenomicPoint: - """Convert a coordinate to a genomic point model (g./m./o.). + """Convert a coordinate dataclass to a genomic point dataclass (g./m./o.). - :arg Coord coordinate: Coordinate model + :arg Coord coordinate: Coordinate dataclass. + :arg int|None length: Length of the sequence. - :returns GenomicPoint: Genomic point model. + :returns GenomicPoint: Genomic point dataclass. """ - _check_in_range(coord.coordinate, length) + if length is not None: + _check_in_range(coord.coordinate, length) return GenomicPoint(coord.coordinate + 1) def genomic_to_coordinate(self, point: GenomicPoint) -> Coord: - """Convert a genomic point (g./m./o.) to a coordinate. + """Convert a genomic point dataclass (g./m./o.) to a coordinate dataclass. - :arg GenomicPoint point: Genomic point model. + :arg GenomicPoint point: Genomic point dataclass. - :returns Coord: Coordinate model. + :returns Coord: Coordinate dataclass. """ return Coord(point.position - 1) @@ -54,7 +56,9 @@ def __post_init__(self) -> None: _check_int(self.offset) if self.region not in self.allowed_regions: - raise ValueError(f'Region must be a string in {self.allowed_regions}.') + raise ValueError( + f'Region {self.region} is invalid, it must be a string from {self.allowed_regions}.' + ) def __str__(self) -> str: if self.offset == 0: @@ -67,9 +71,15 @@ def __str__(self) -> str: class NonCoding(Genomic): """NonCoding crossmap object.""" - def __init__(self, locations: list[tuple[int, int]], length: int | None = None, inverted: bool = False) -> None: + def __init__( + self, + locations: list[tuple[int, int]], + length: int | None = None, + inverted: bool = False, + ) -> None: """ :arg list locations: List of locus locations. + :arg int|None length: Length of the reference sequence. :arg bool inverted: Orientation. """ _check_multi_locus(locations, length) @@ -77,11 +87,11 @@ def __init__(self, locations: list[tuple[int, int]], length: int | None = None, self._noncoding = MultiLocus(locations, length, inverted) def coordinate_to_noncoding(self, coord: Coord) -> NonCodingPoint: - """Convert a coordinate to a noncoding point model (n./r.). + """Convert a coordinate dataclass to a noncoding point dataclass (n./r.). - :arg Coord coord: Coordinate model. + :arg Coord coord: Coordinate dataclass. - :returns NonCodingPoint: Noncoding point model. + :returns NonCodingPoint: Noncoding point dataclass. """ point = self._noncoding.to_position(coord) return NonCodingPoint( @@ -91,13 +101,12 @@ def coordinate_to_noncoding(self, coord: Coord) -> NonCodingPoint: ) def noncoding_to_coordinate(self, point: NonCodingPoint) -> Coord: - """Convert a noncoding point (n./r.) to a coordinate model. + """Convert a noncoding point dataclass (n./r.) to a coordinate dataclass. - :arg NonCodingPoint point: Noncoding point model. + :arg NonCodingPoint point: Noncoding point dataclass. - :returns Coord: Coordinate model. + :returns Coord: Coordinate dataclass. """ - # Catch errors from multi_locus module try: return self._noncoding.to_coordinate( Point( @@ -106,14 +115,14 @@ def noncoding_to_coordinate(self, point: NonCodingPoint) -> Coord: region=point.region ) ) - except ValueError as e: - if 'Position' in str(e): - raise ValueError(str(e).replace(str(point.position - 1), str(point.position))) - raise e - except IndexError as e: - if 'Position' in str(e): - raise IndexError(str(e).replace(str(point.position - 1), str(point.position))) - raise e + except ValueError as error: + if 'Position' in str(error): + raise ValueError(str(error).replace(str(point.position - 1), str(point.position))) + raise error + except IndexError as error: + if 'Position' in str(error): + raise IndexError(str(error).replace(str(point.position - 1), str(point.position))) + raise error @dataclass(slots=True) @@ -151,6 +160,7 @@ def __init__( """ :arg list locations: List of locus locations. :arg tuple cds: Locus location. + :arg int|None length: Length of the reference sequence. :arg bool inverted: Orientation. """ NonCoding.__init__(self, locations, length, inverted) @@ -180,10 +190,16 @@ def __init__( exon_end.position + exon_end.offset + 1 ) - def _check_cds(self, cds: tuple[int, int], locations: list[tuple[int, int]], length: int|None = None) -> None: + def _check_cds( + self, + cds: tuple[int, int], + locations: list[tuple[int, int]], + length: int | None = None, + ) -> None: """Check if the CDS is valid.""" _check_locus(cds) - _check_in_range(cds[1], length) + if length is not None: + _check_in_range(cds[1], length) for coord in cds: index = _nearest_location(locations, coord) if coord < locations[index][0] or coord > locations[index][1]: @@ -192,7 +208,8 @@ def _check_cds(self, cds: tuple[int, int], locations: list[tuple[int, int]], len def _validate_point(self, position: int, region: str) -> None: """Validate a coding point model under HGVS rules. - :arg CodingPoint point: Coding point model. + :arg int position: Position. + :arg str region: Region. """ if region == 'u': if position not in (1, self._coding[0]): @@ -212,11 +229,11 @@ def _validate_point(self, position: int, region: str) -> None: def _coordinate_to_coding(self, coord: Coord) -> CodingPoint: - """Convert a coordinate to a coding point model (c./r.). + """Convert a coordinate dataclass to a coding point dataclass (c./r.). - :arg Coord coord: Coordinate model. + :arg Coord coord: Coordinate dataclass. - :returns CodingPoint: Coding position model (c./r.). + :returns CodingPoint: Coding point dataclass (c./r.). """ noncoding_point = self._noncoding.to_position(coord) @@ -246,12 +263,12 @@ def _coordinate_to_coding(self, coord: Coord) -> CodingPoint: return CodingPoint(position=position, offset=offset, region=region) def coordinate_to_coding(self, coord: Coord, degenerate: bool = False) -> CodingPoint: - """Convert a coordinate to a coding point model (c./r.). + """Convert a coordinate dataclass to a coding point dataclass (c./r.). - :arg Coord coord: Coordinate model. - :arg bool degenerate: Return a degenerate position. + :arg Coord coord: Coordinate dataclass. + :arg bool degenerate: Return a degenerate coding point dataclass. - :returns CodingPoint: Coding point model (c./r.). + :returns CodingPoint: Coding point dataclass (c./r.). """ point = self._coordinate_to_coding(coord) @@ -274,11 +291,11 @@ def coordinate_to_coding(self, coord: Coord, degenerate: bool = False) -> Coding return point def _coding_to_coordinate(self, point: CodingPoint) -> Coord: - """Convert a coding position (c./r.) to a coordinate. + """Convert a coding point dataclass (c./r.) to a coordinate dataclass. - :arg CodingPoint point: Coding point model (c./r.). + :arg CodingPoint point: Coding point dataclass (c./r.). - :returns int: Coordinate. + :returns Coord: Coordinate dataclass. """ region = point.region position = point.position @@ -309,41 +326,52 @@ def _coding_to_coordinate(self, point: CodingPoint) -> Coord: return self._noncoding.to_coordinate( Point(position=position, region='', offset=point.offset) ) - except ValueError as e: - if 'Position' in str(e): - raise ValueError(str(e).replace(str(position), str(point.position))) - raise e - except IndexError as e: - if 'Position' in str(e): - raise IndexError(str(e).replace(str(position), str(point.position))) - raise e + except ValueError as error: + if 'Position' in str(error): + raise ValueError(str(error).replace(str(position), str(point.position))) def coding_to_coordinate(self, point: CodingPoint) -> Coord: - """Convert a coding position (c./r.) to a coordinate. + """Convert a coding point dataclass (c./r.) to a coordinate dataclass. - :arg CodingPoint point: Coding point model (c./r.). + :arg CodingPoint point: Coding point dataclass (c./r.). - :returns Coord: Coordinate module. + :returns Coord: Coordinate dataclass. """ # Silently correct for degenerate points if point.offset == 0: if point.region == '-' and point.position > self._coding[0]: if self._coding[0] == 0: - return self._coding_to_coordinate(CodingPoint(position=1, offset=self._coding[0] - point.position, region='u')) - return self._coding_to_coordinate(CodingPoint(position=self._coding[0], offset=self._coding[0] - point.position, region='u')) + return self._coding_to_coordinate(CodingPoint( + position=1, + offset=self._coding[0] - point.position, + region='u', + )) + return self._coding_to_coordinate(CodingPoint( + position=self._coding[0], + offset=self._coding[0] - point.position, + region='u', + )) if point.region == '*' and point.position > self._exons[1] - self._coding[1]: if self._exons[1] == self._coding[1]: - return self._coding_to_coordinate(CodingPoint(position=1, offset=point.position - (self._exons[1] - self._coding[1]), region='d')) - return self._coding_to_coordinate(CodingPoint(position=self._exons[1] - self._coding[1], offset=point.position - (self._exons[1] - self._coding[1]), region='d')) + return self._coding_to_coordinate(CodingPoint( + position=1, + offset=point.position - (self._exons[1] - self._coding[1]), + region='d', + )) + return self._coding_to_coordinate(CodingPoint( + position=self._exons[1] - self._coding[1], + offset=point.position - (self._exons[1] - self._coding[1]), + region='d', + )) return self._coding_to_coordinate(point) def coordinate_to_protein(self, coord: Coord) -> ProteinPoint: - """Convert a coordinate to a protein point model (p.). + """Convert a coordinate dataclass to a protein point dataclass (p.). - :arg Coord coord: Coordinate model. + :arg Coord coord: Coordinate dataclass. - :returns ProteinPoint: Protein point model(p.). + :returns ProteinPoint: Protein point dataclass (p.). """ point = self.coordinate_to_coding(coord) @@ -363,21 +391,21 @@ def coordinate_to_protein(self, coord: Coord) -> ProteinPoint: ) def protein_to_coordinate(self, point: ProteinPoint) -> Coord: - """Convert a protein position (p.) to a coordinate. + """Convert a protein point dataclass (p.) to a coordinate dataclass. - :arg ProteinPoint point: Protein point model(p.). + :arg ProteinPoint point: Protein point dataclass (p.). - :returns Coord: Coordinate module. + :returns Coord: Coordinate dataclass. """ if point.region in ('-', 'u'): - return self.coding_to_coordinate( + return self._coding_to_coordinate( CodingPoint( position=3 * point.position - point.position_in_codon + 1, offset=point.offset, region=point.region ) ) - return self.coding_to_coordinate( + return self._coding_to_coordinate( CodingPoint( position=3 * point.position + point.position_in_codon - 3, offset=point.offset, diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 8ce1687..4aa1fea 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -9,15 +9,15 @@ def test_GenomicPoint_invalid_initialization(): """GenomicPoint cannot be initialized with invalid position.""" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: GenomicPoint(position=0) - assert str(e.value) == 'Position 0 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 must be a positive integer.' + with pytest.raises(ValueError) as error: GenomicPoint(position=-1) - assert str(e.value) == 'Position -1 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position -1 must be a positive integer.' + with pytest.raises(ValueError) as error: GenomicPoint(position=[101]) - assert str(e.value) == 'Value must be an integer.' + assert str(error.value) == 'Value must be an integer.' def test_Genomic(): @@ -41,12 +41,12 @@ def test_Genomic(): def test_Genomic_invalid_with_length(): """Raise ValueError if coordinate is out of bounds.""" crossmap = Genomic() - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: crossmap.coordinate_to_genomic(Coord(-1), 99) - assert str(e.value) == 'Value must be non-negative.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be non-negative.' + with pytest.raises(ValueError) as error: crossmap.coordinate_to_genomic(Coord(99), 99) - assert str(e.value) == 'Value 99 must be within the bounds of the reference length 99.' + assert str(error.value) == 'Location 99 must be within the bounds of the reference length 99.' def test_Genomic_with_length(): @@ -69,74 +69,75 @@ def test_Genomic_with_length(): def test_NonCodingPoint_invalid_initialization(): """Raise error with invalid initialization.""" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: NonCodingPoint(position=0, offset=0, region='u') - assert str(e.value) == 'Position 0 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 must be a positive integer.' + with pytest.raises(ValueError) as error: NonCodingPoint(position=0, offset=0, region='d') - assert str(e.value) == 'Position 0 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 must be a positive integer.' + with pytest.raises(ValueError) as error: NonCodingPoint(position=0, offset=0, region='') - assert str(e.value) == 'Position 0 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 must be a positive integer.' + with pytest.raises(ValueError) as error: NonCodingPoint(position=0, offset=0, region='*') - assert str(e.value) == 'Position 0 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 must be a positive integer.' + with pytest.raises(ValueError) as error: NonCodingPoint(position=-1, offset=0, region='') - assert str(e.value) == 'Position -1 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position -1 must be a positive integer.' + with pytest.raises(ValueError) as error: NonCodingPoint(position=1, offset=None, region='u') - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: NonCodingPoint(position=1, offset=1, region='-') - assert str(e.value) == "Region must be a string in ['', 'u', 'd']." + assert str(error.value) == "Region - is invalid, it must be a string from ['', 'u', 'd']." def test_NonCoding_invalid(): """Raise ValueError if noncoding is invalid.""" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: NonCoding([()]) - assert str(e.value) == 'Locus must be a tuple of two values.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus must be a tuple of two values.' + with pytest.raises(ValueError) as error: NonCoding([(10)]) - assert str(e.value) == 'Locus must be a tuple of two values.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus must be a tuple of two values.' + with pytest.raises(ValueError) as error: NonCoding([(10, 20), (15, 25)]) - assert str(e.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' + with pytest.raises(ValueError) as error: NonCoding([(None, 20), (30, None)]) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: NonCoding(_exons, length=70) - assert str(e.value) == 'Value 72 must be within the bounds of the reference length 70.' + assert str(error.value) == 'Location 72 must be within the bounds of the reference length 70.' # Reverse orientation - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: NonCoding([()], inverted=True) - assert str(e.value) == 'Locus must be a tuple of two values.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus must be a tuple of two values.' + with pytest.raises(ValueError) as error: NonCoding([(10)], inverted=True) - assert str(e.value) == 'Locus must be a tuple of two values.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus must be a tuple of two values.' + with pytest.raises(ValueError) as error: NonCoding([(10, 20), (15, 25)], inverted=True) - assert str(e.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' + with pytest.raises(ValueError) as error: NonCoding([(None, 20), (30, None)], inverted=True) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: NonCoding(_exons, length=70, inverted=True) - assert str(e.value) == 'Value 72 must be within the bounds of the reference length 70.' + assert str(error.value) == 'Location 72 must be within the bounds of the reference length 70.' def test_NonCoding_invalid_with_length(): """Raise ValueError if coordinate is out of bounds.""" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: NonCoding(_exons, length=70) - assert str(e.value) == 'Value 72 must be within the bounds of the reference length 70.' + assert str(error.value) == 'Location 72 must be within the bounds of the reference length 70.' + # Reverse orientation - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: NonCoding(_exons, length=70, inverted=True) - assert str(e.value) == 'Value 72 must be within the bounds of the reference length 70.' + assert str(error.value) == 'Location 72 must be within the bounds of the reference length 70.' def test_NonCoding(): @@ -223,12 +224,12 @@ def test_NonCoding_with_length(): crossmap.noncoding_to_coordinate, NonCodingPoint(position=22, offset=3, region='d'), ) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: crossmap.coordinate_to_noncoding(Coord(75)) - assert str(e.value) == 'Value 75 must be within the bounds of the reference length 75.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Location 75 must be within the bounds of the reference length 75.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=4, region='d')) - assert str(e.value) == 'Offset 4 exceeds downstream region.' + assert str(error.value) == 'Offset 4 exceeds downstream region.' def test_NonCoding_inverted(): @@ -269,12 +270,12 @@ def test_NonCoding_inverted_with_length(): crossmap = NonCoding(_exons, length=75, inverted=True) # Boundary between upstream and sequence end. - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: crossmap.coordinate_to_noncoding(Coord(75)) - assert str(e.value) == 'Value 75 must be within the bounds of the reference length 75.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Location 75 must be within the bounds of the reference length 75.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-4, region='u')) - assert str(e.value) == 'Offset -4 exceeds upstream region.' + assert str(error.value) == 'Offset -4 exceeds upstream region.' invariant( crossmap.coordinate_to_noncoding, Coord(74), @@ -314,187 +315,187 @@ def test_NonCoding_inverted_with_length(): def test_NonCoding_invalid_position(): """Raise error if position is not valid under HGVS rules.""" crossmap = NonCoding(_exons, length=75) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=0, offset=1, region='u')) - assert str(e.value) == 'Position 0 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 must be a positive integer.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=2, offset=1, region='u')) - assert str(e.value) == 'Position 2 is not at upstream boundary.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Position 2 is not at upstream boundary.' + with pytest.raises(IndexError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=23, offset=0, region='')) - assert str(e.value) == 'Position 23 exceeds multi locus length.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 23 exceeds multi locus length.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=21, offset=1, region='d')) - assert str(e.value) == 'Position 21 is not at downstream boundary.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 21 is not at downstream boundary.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=30, offset=-1, region='d')) - assert str(e.value) == 'Position 30 is not at downstream boundary.' + assert str(error.value) == 'Position 30 is not at downstream boundary.' def test_NonCoding_invalid_position_inverted(): """Raise error if position is not valid under HGVS rules.""" crossmap = NonCoding(_exons, length=75, inverted=True) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=0, offset=1, region='u')) - assert str(e.value) == 'Position 0 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 must be a positive integer.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=2, offset=1, region='u')) - assert str(e.value) == 'Position 2 is not at upstream boundary.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Position 2 is not at upstream boundary.' + with pytest.raises(IndexError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=23, offset=0, region='')) - assert str(e.value) == 'Position 23 exceeds multi locus length.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 23 exceeds multi locus length.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=21, offset=1, region='d')) - assert str(e.value) == 'Position 21 is not at downstream boundary.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 21 is not at downstream boundary.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=30, offset=-1, region='d')) - assert str(e.value) == 'Position 30 is not at downstream boundary.' + assert str(error.value) == 'Position 30 is not at downstream boundary.' def test_NonCoding_invalid_offset(): """Raise error if offset is not valid under HGVS rules.""" crossmap = NonCoding(_exons, length=75) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=0, region='u')) - assert e.value.args[0] == 'Offset 0 at upstream boundary should be negative.' - with pytest.raises(ValueError) as e: + assert error.value.args[0] == 'Offset 0 at upstream boundary should be negative.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=1, region='u')) - assert e.value.args[0] == 'Offset 1 at upstream boundary should be negative.' - with pytest.raises(ValueError) as e: + assert error.value.args[0] == 'Offset 1 at upstream boundary should be negative.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-6, region='u')) - assert e.value.args[0] == 'Offset -6 exceeds upstream boundary.' - with pytest.raises(ValueError) as e: + assert error.value.args[0] == 'Offset -6 exceeds upstream boundary.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-1, region='')) - assert e.value.args[0] == 'Offset -1 at the first exon should be in the upstream region.' - with pytest.raises(IndexError) as e: + assert error.value.args[0] == 'Offset -1 at the first exon should be in the upstream region.' + with pytest.raises(IndexError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=1, region='')) - assert e.value.args[0] == 'Offset 1 should be at a locus end.' - with pytest.raises(IndexError) as e: + assert error.value.args[0] == 'Offset 1 should be at a locus end.' + with pytest.raises(IndexError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=10, offset=1, region='')) - assert e.value.args[0] == 'Offset 1 should be at a locus end.' - with pytest.raises(IndexError) as e: + assert error.value.args[0] == 'Offset 1 should be at a locus end.' + with pytest.raises(IndexError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=10, offset=-11, region='')) - assert e.value.args[0] == 'Offset -11 exceeds intron length.' - with pytest.raises(IndexError) as e: + assert error.value.args[0] == 'Offset -11 exceeds intron length.' + with pytest.raises(IndexError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=-1, region='')) - assert e.value.args[0] == 'Offset -1 should be at a locus start.' - with pytest.raises(ValueError) as e: + assert error.value.args[0] == 'Offset -1 should be at a locus start.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=1, region='')) - assert e.value.args[0] == 'Offset 1 at the first exon should be in the downstream region.' - with pytest.raises(ValueError) as e: + assert error.value.args[0] == 'Offset 1 at the first exon should be in the downstream region.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=0, region='d')) - assert e.value.args[0] == 'Offset 0 at downstream boundary should be positive.' - with pytest.raises(ValueError) as e: + assert error.value.args[0] == 'Offset 0 at downstream boundary should be positive.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=-1, region='d')) - assert e.value.args[0] == 'Offset -1 at downstream boundary should be positive.' + assert error.value.args[0] == 'Offset -1 at downstream boundary should be positive.' def test_NonCoding_invalid_offset_inverted(): """Raise error if offset is not valid under HGVS rules.""" crossmap = NonCoding(_exons, length=75, inverted=True) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=0, region='u')) - assert e.value.args[0] == 'Offset 0 at upstream boundary should be negative.' - with pytest.raises(ValueError) as e: + assert error.value.args[0] == 'Offset 0 at upstream boundary should be negative.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=1, region='u')) - assert e.value.args[0] == 'Offset 1 at upstream boundary should be negative.' - with pytest.raises(ValueError) as e: + assert error.value.args[0] == 'Offset 1 at upstream boundary should be negative.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-5, region='u')) - assert e.value.args[0] == 'Offset -5 exceeds upstream boundary.' - with pytest.raises(ValueError) as e: + assert error.value.args[0] == 'Offset -5 exceeds upstream boundary.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-1, region='')) - assert e.value.args[0] == 'Offset -1 at the first exon should be in the upstream region.' - with pytest.raises(IndexError) as e: + assert error.value.args[0] == 'Offset -1 at the first exon should be in the upstream region.' + with pytest.raises(IndexError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=1, region='')) - assert e.value.args[0] == 'Offset 1 should be at a locus end.' - with pytest.raises(IndexError) as e: + assert error.value.args[0] == 'Offset 1 should be at a locus end.' + with pytest.raises(IndexError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=13, offset=-1, region='')) - assert e.value.args[0] == 'Offset -1 should be at a locus end.' - with pytest.raises(IndexError) as e: + assert error.value.args[0] == 'Offset -1 should be at a locus end.' + with pytest.raises(IndexError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=13, offset=11, region='')) - assert e.value.args[0] == 'Offset 11 exceeds intron length.' - with pytest.raises(IndexError) as e: + assert error.value.args[0] == 'Offset 11 exceeds intron length.' + with pytest.raises(IndexError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=-1, region='')) - assert e.value.args[0] == 'Offset -1 should be at a locus start.' - with pytest.raises(ValueError) as e: + assert error.value.args[0] == 'Offset -1 should be at a locus start.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=1, region='')) - assert e.value.args[0] == 'Offset 1 at the last exon on the reverse complement should be in the downstream region.' - with pytest.raises(ValueError) as e: + assert error.value.args[0] == 'Offset 1 at the last exon on the reverse complement should be in the downstream region.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=0, region='d')) - assert e.value.args[0] == 'Offset 0 at downstream boundary should be positive.' - with pytest.raises(ValueError) as e: + assert error.value.args[0] == 'Offset 0 at downstream boundary should be positive.' + with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=-1, region='d')) - assert e.value.args[0] == 'Offset -1 at downstream boundary should be positive.' + assert error.value.args[0] == 'Offset -1 at downstream boundary should be positive.' def test_CodingPoint_invalid_initialization(): """Raise error with invalid initialization.""" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: CodingPoint(position=0, offset=0, region='-') - assert str(e.value) == 'Position 0 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 must be a positive integer.' + with pytest.raises(ValueError) as error: CodingPoint(position=0, offset=0, region='*') - assert str(e.value) == 'Position 0 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 must be a positive integer.' + with pytest.raises(ValueError) as error: CodingPoint(position=0, offset=0, region='') - assert str(e.value) == 'Position 0 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 must be a positive integer.' + with pytest.raises(ValueError) as error: CodingPoint(position=-1, offset=0, region='') - assert str(e.value) == 'Position -1 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position -1 must be a positive integer.' + with pytest.raises(ValueError) as error: CodingPoint(position=1, offset=None, region='') - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: CodingPoint(position=2, offset=1, region='upstream') - assert str(e.value) == "Region must be a string in ['', 'u', 'd', '-', '*']." + assert str(error.value) == "Region upstream is invalid, it must be a string from ['', 'u', 'd', '-', '*']." def test_Coding_invalid(): """Raise ValueError if coding is invalid.""" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: Coding([(20, 20)], (20, 20)) - assert str(e.value) == 'Locus start 20 must be smaller than locus end 20.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus start 20 must be smaller than locus end 20.' + with pytest.raises(ValueError) as error: Coding([(10, 20)], (9,15)) - assert str(e.value) == 'Coordinate 9 of CDS (9, 15) is not within any exon.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Coordinate 9 of CDS (9, 15) is not within any exon.' + with pytest.raises(ValueError) as error: Coding([(10, 20)], (10,21)) - assert str(e.value) == 'Coordinate 21 of CDS (10, 21) is not within any exon.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Coordinate 21 of CDS (10, 21) is not within any exon.' + with pytest.raises(ValueError) as error: Coding([(10, 20)], (15, 10)) - assert str(e.value) == 'Locus start 15 must be smaller than locus end 10.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus start 15 must be smaller than locus end 10.' + with pytest.raises(ValueError) as error: Coding([], None) - assert str(e.value) == 'Locations must be a non-empty list of tuples.' + assert str(error.value) == 'Locations must be a non-empty list of tuples.' # Reverse orientation - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: Coding([(20, 20)], (20, 20), inverted=True) - assert str(e.value) == 'Locus start 20 must be smaller than locus end 20.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus start 20 must be smaller than locus end 20.' + with pytest.raises(ValueError) as error: Coding([(10, 20)], (9,15), inverted=True) - assert str(e.value) == 'Coordinate 9 of CDS (9, 15) is not within any exon.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Coordinate 9 of CDS (9, 15) is not within any exon.' + with pytest.raises(ValueError) as error: Coding([(10, 20)], (10,21), inverted=True) - assert str(e.value) == 'Coordinate 21 of CDS (10, 21) is not within any exon.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Coordinate 21 of CDS (10, 21) is not within any exon.' + with pytest.raises(ValueError) as error: Coding([(10, 20)], (15, 10), inverted=True) - assert str(e.value) == 'Locus start 15 must be smaller than locus end 10.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Locus start 15 must be smaller than locus end 10.' + with pytest.raises(ValueError) as error: Coding([], None, inverted=True) - assert str(e.value) == 'Locations must be a non-empty list of tuples.' + assert str(error.value) == 'Locations must be a non-empty list of tuples.' def test_Coding_invalid_with_length(): """Raise ValueError if coordinate is out of bounds.""" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: Coding(_exons, _cds, length=70) - assert str(e.value) == 'Value 72 must be within the bounds of the reference length 70.' + assert str(error.value) == 'Location 72 must be within the bounds of the reference length 70.' # Reverse orientation - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: Coding(_exons, _cds, length=70, inverted=True) - assert str(e.value) == 'Value 72 must be within the bounds of the reference length 70.' + assert str(error.value) == 'Location 72 must be within the bounds of the reference length 70.' def test_Coding(): @@ -624,9 +625,9 @@ def test_Coding_with_length(): crossmap.coding_to_coordinate, CodingPoint(position=5, offset=3, region='d'), ) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: crossmap.coordinate_to_coding(Coord(75)) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=5, offset=4, region='d')) @@ -696,12 +697,12 @@ def test_Coding_inverted_with_length(): crossmap = Coding(_exons, _cds, length=75, inverted=True) # Boundary between upstream and sequence end. - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: crossmap.coordinate_to_coding(Coord(75)) - assert str(e.value) == 'Value 75 must be within the bounds of the reference length 75.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Location 75 must be within the bounds of the reference length 75.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=5, offset=-4, region='u')) - assert str(e.value) == 'Offset -4 exceeds upstream region.' + assert str(error.value) == 'Offset -4 exceeds upstream region.' invariant( crossmap.coordinate_to_coding, Coord(74), @@ -1279,145 +1280,145 @@ def test_Coding_invalid_position(): """Raise error if position in coding point is invalid.""" crossmap = Coding(_exons, _cds) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=0, offset=1, region='u')) - assert str(e.value) == 'Position 0 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 must be a positive integer.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=12, offset=-1, region='u')) - assert str(e.value) == 'Position 12 is not in upstream boundary.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 12 is not in upstream boundary.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=13, offset=1, region='-')) - assert str(e.value) == 'Position 13 exceeds - region.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 13 exceeds - region.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=-1, offset=0, region='-')) - assert str(e.value) == 'Position -1 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position -1 must be a positive integer.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=0, offset=0, region='')) - assert str(e.value) == 'Position 0 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 must be a positive integer.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=7, offset=0, region='')) - assert str(e.value) == 'Position 7 exceeds coding region.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 7 exceeds coding region.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=6, offset=1, region='*')) - assert str(e.value) == 'Position 6 exceeds * region.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 6 exceeds * region.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=None, offset=0, region='*')) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=6, offset=0, region='d')) - assert str(e.value) == 'Position 6 is not in downstream boundary.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 6 is not in downstream boundary.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=1000, offset=2, region='d')) - assert str(e.value) == 'Position 1000 is not in downstream boundary.' + assert str(error.value) == 'Position 1000 is not in downstream boundary.' def test_Coding_inverted_invalid_position_inverted(): """Raise error if position in coding point is invalid for inverted coding.""" crossmap = Coding(_exons, _cds, inverted=True) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=0, offset=1, region='u')) - assert str(e.value) == 'Position 0 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 must be a positive integer.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=12, offset=-1, region='u')) - assert str(e.value) == 'Position 12 is not in upstream boundary.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 12 is not in upstream boundary.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=13, offset=1, region='-')) - assert str(e.value) == 'Position 13 exceeds - region.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 13 exceeds - region.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=-1, offset=0, region='-')) - assert str(e.value) == 'Position -1 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position -1 must be a positive integer.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=0, offset=0, region='')) - assert str(e.value) == 'Position 0 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 must be a positive integer.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=7, offset=0, region='')) - assert str(e.value) == 'Position 7 exceeds coding region.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 7 exceeds coding region.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=13, offset=1, region='*')) - assert str(e.value) == 'Position 13 exceeds * region.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 13 exceeds * region.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=None, offset=0, region='*')) - assert str(e.value) == 'Value must be an integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=6, offset=0, region='d')) - assert str(e.value) == 'Position 6 is not in downstream boundary.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 6 is not in downstream boundary.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=1000, offset=2, region='d')) - assert str(e.value) == 'Position 1000 is not in downstream boundary.' + assert str(error.value) == 'Position 1000 is not in downstream boundary.' def test_Coding_invalid_offset(): """Raise error if offset in coding point is invalid.""" crossmap = Coding(_exons, _cds, length=75) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-6, region='u')) - assert str(e.value) == 'Offset -6 exceeds upstream region.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset -6 exceeds upstream region.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=1, region='-')) - assert str(e.value) == 'Position 1 is not at a locus boundary.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 1 is not at a locus boundary.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-1, region='-')) - assert str(e.value) == 'Position 1 is not at a locus boundary.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 1 is not at a locus boundary.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-1, region='')) - assert str(e.value) == 'Position 1 is not at a locus boundary.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Position 1 is not at a locus boundary.' + with pytest.raises(IndexError) as error: crossmap.coding_to_coordinate(CodingPoint(position=3, offset=6, region='')) - assert str(e.value) == 'Offset 6 exceeds intron length.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset 6 exceeds intron length.' + with pytest.raises(IndexError) as error: crossmap.coding_to_coordinate(CodingPoint(position=4, offset=12, region='*')) - assert str(e.value) == 'Offset 12 should be at a locus end.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset 12 should be at a locus end.' + with pytest.raises(IndexError) as error: crossmap.coding_to_coordinate(CodingPoint(position=4, offset=-50, region='*')) - assert str(e.value) == 'Offset -50 exceeds intron length.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset -50 exceeds intron length.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=5, offset=10, region='d')) - assert str(e.value) == 'Offset 10 exceeds downstream region.' + assert str(error.value) == 'Offset 10 exceeds downstream region.' def test_Coding_invalid_offset_inverted(): """Raise error if offset in coding point is invalid.""" crossmap = Coding(_exons, _cds, length=75, inverted=True) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-6, region='u')) - assert str(e.value) == 'Offset -6 exceeds upstream region.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset -6 exceeds upstream region.' + with pytest.raises(IndexError) as error: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=1, region='-')) - assert str(e.value) == 'Offset 1 should be at a locus end.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset 1 should be at a locus end.' + with pytest.raises(IndexError) as error: crossmap.coding_to_coordinate(CodingPoint(position=2, offset=-1, region='-')) - assert str(e.value) == 'Offset -1 should be at a locus start.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset -1 should be at a locus start.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-1, region='')) - assert str(e.value) == 'Position 1 is not at a locus boundary.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Position 1 is not at a locus boundary.' + with pytest.raises(IndexError) as error: crossmap.coding_to_coordinate(CodingPoint(position=3, offset=6, region='')) - assert str(e.value) == 'Offset 6 exceeds intron length.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset 6 exceeds intron length.' + with pytest.raises(IndexError) as error: crossmap.coding_to_coordinate(CodingPoint(position=4, offset=12, region='*')) - assert str(e.value) == 'Offset 12 exceeds intron length.' - with pytest.raises(IndexError) as e: + assert str(error.value) == 'Offset 12 exceeds intron length.' + with pytest.raises(IndexError) as error: crossmap.coding_to_coordinate(CodingPoint(position=4, offset=-50, region='*')) - assert str(e.value) == 'Offset -50 exceeds intron length.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Offset -50 exceeds intron length.' + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=11, offset=10, region='d')) - assert str(e.value) == 'Offset 10 exceeds downstream region.' + assert str(error.value) == 'Offset 10 exceeds downstream region.' def test_Coding_protein_point_invalid_initialization(): """Raise error if protein point is initialized with invalid values.""" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError) as error: ProteinPoint(position=0, offset=0, region='u', position_in_codon=1) - assert str(e.value) == 'Position 0 must be a positive integer.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position 0 must be a positive integer.' + with pytest.raises(ValueError) as error: ProteinPoint(position=1, offset=0, region='', position_in_codon=4) - assert str(e.value) == 'Position_in_codon must be 1, 2, or 3.' - with pytest.raises(ValueError) as e: + assert str(error.value) == 'Position_in_codon must be 1, 2, or 3.' + with pytest.raises(ValueError) as error: ProteinPoint(position=1, offset=0, region='', position_in_codon=0) - assert str(e.value) == 'Position_in_codon must be 1, 2, or 3.' + assert str(error.value) == 'Position_in_codon must be 1, 2, or 3.' def test_Coding_protein(): From 2d113c0cea92f7be73e4d341ed352107ec989b8d Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 1 Sep 2026 09:57:11 +0200 Subject: [PATCH 205/236] Fix typings. --- mutalyzer_crossmapper/crossmapper.py | 1 + mutalyzer_crossmapper/multi_locus.py | 1 + 2 files changed, 2 insertions(+) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 525aa55..0f04a4b 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -329,6 +329,7 @@ def _coding_to_coordinate(self, point: CodingPoint) -> Coord: except ValueError as error: if 'Position' in str(error): raise ValueError(str(error).replace(str(position), str(point.position))) + raise def coding_to_coordinate(self, point: CodingPoint) -> Coord: """Convert a coding point dataclass (c./r.) to a coordinate dataclass. diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 52a5134..a028faa 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -242,6 +242,7 @@ def to_coordinate(self, point: Point) -> Coord: str(point.position - self._offsets[index]), str(point.position) ) ) from error + raise except IndexError as error: if "Position" in str(error): raise IndexError( From f371e05a0138b2c450f4f88796d19e8bedd08ada Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 2 Sep 2026 09:02:47 +0200 Subject: [PATCH 206/236] Use '' for string. --- mutalyzer_crossmapper/multi_locus.py | 58 ++++++++++++++-------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index a028faa..6384eb1 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -11,13 +11,13 @@ class Point(LocusPoint): """Point dataclass""" - region: str = "" + region: str = '' def __post_init__(self) -> None: LocusPoint.__post_init__(self) - if self.region not in ("", "u", "d"): + if self.region not in ('', 'u', 'd'): raise ValueError( - f"Region {self.region} is invalid, it must be a string from '', 'u' or 'd'." + f'Region {self.region} is invalid, it must be a string from "", "u" or "d".' ) @@ -25,21 +25,21 @@ def _check_in_range(value: int, length: int) -> None: """Check if the value no larger than length.""" if value >= length: raise ValueError( - f"Location {value} must be within the bounds of the reference length {length}." + f'Location {value} must be within the bounds of the reference length {length}.' ) def _check_multi_locus(locations: list[tuple[int, int]], length: int | None = None) -> None: """Check if the locations list is valid.""" if not locations or not isinstance(locations, list): - raise ValueError("Locations must be a non-empty list of tuples.") + raise ValueError('Locations must be a non-empty list of tuples.') for locus in locations: _check_locus(locus) for l1, l2 in zip(locations, locations[1:]): if l2[0] < l1[1]: - raise ValueError(f"Locus {l2} and locus {l1} are overlapping.") + raise ValueError(f'Locus {l2} and locus {l1} are overlapping.') if length is not None: _check_in_range(locations[-1][1], length) @@ -94,45 +94,45 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - :arg int offset: Offset. :arg str region: Region. """ - if region == "u": + if region == 'u': if position != 0: - raise ValueError(f"Position {position} is not at upstream boundary.") + raise ValueError(f'Position {position} is not at upstream boundary.') if offset >= 0: - raise ValueError(f"Offset {offset} at upstream region should be negative.") + raise ValueError(f'Offset {offset} at upstream region should be negative.') if self._inverted: if ( self._length is not None and -offset >= self._length - self._loci[self._direction(0)].boundary[1] ): - raise ValueError(f"Offset {offset} exceeds upstream region.") + raise ValueError(f'Offset {offset} exceeds upstream region.') else: if -offset > self._loci[self._direction(0)].boundary[0]: - raise ValueError(f"Offset {offset} exceeds upstream region.") + raise ValueError(f'Offset {offset} exceeds upstream region.') - if region == "d": + if region == 'd': if position != self._end - 1: - raise ValueError(f"Position {position} is not at downstream boundary.") + raise ValueError(f'Position {position} is not at downstream boundary.') if offset <= 0: - raise ValueError(f"Offset {offset} at downstream region should be positive.") + raise ValueError(f'Offset {offset} at downstream region should be positive.') if not self._inverted: if ( self._length is not None and offset >= self._length - self._loci[self._direction(-1)].boundary[1] ): - raise ValueError(f"Offset {offset} exceeds downstream region.") + raise ValueError(f'Offset {offset} exceeds downstream region.') else: if ( offset > self._loci[self._direction(len(self._locations) - 1)].boundary[0] ): - raise ValueError(f"Offset {offset} exceeds downstream region.") + raise ValueError(f'Offset {offset} exceeds downstream region.') - if region == "": + if region == '': if offset < 0: if index == 0 and position == 0: - raise ValueError(f"Offset {offset} at the first locus should be in the upstream region.") + raise ValueError(f'Offset {offset} at the first locus should be in the upstream region.') if self._inverted: if ( self._direction(index) != len(self._loci) - 1 @@ -140,7 +140,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - >= self._loci[self._direction(index - 1)].boundary[0] - self._loci[self._direction(index)].boundary[1] ): - raise IndexError(f"Offset {offset} exceeds intron length.") + raise IndexError(f'Offset {offset} exceeds intron length.') else: if ( self._direction(index) != 0 @@ -148,10 +148,10 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - >= self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index - 1)].boundary[1] ): - raise IndexError(f"Offset {offset} exceeds intron length.") + raise IndexError(f'Offset {offset} exceeds intron length.') if offset > 0: if index == len(self._loci) - 1 and position == self._end - 1: - raise ValueError(f"Offset {offset} at the last locus should be in the downstream region.") + raise ValueError(f'Offset {offset} at the last locus should be in the downstream region.') if self._inverted: if ( self._direction(index) != 0 @@ -159,7 +159,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - >= self._loci[self._direction(index)].boundary[0] - self._loci[self._direction(index + 1)].boundary[1] ): - raise IndexError(f"Offset {offset} exceeds intron length.") + raise IndexError(f'Offset {offset} exceeds intron length.') else: if ( self._direction(index) != len(self._loci) - 1 @@ -167,7 +167,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - >= self._loci[self._direction(index + 1)].boundary[0] - self._loci[self._direction(index)].boundary[1] ): - raise IndexError(f"Offset {offset} exceeds intron length.") + raise IndexError(f'Offset {offset} exceeds intron length.') def _direction(self, index: int) -> int: if self._inverted: @@ -197,7 +197,7 @@ def to_position(self, coord: Coord) -> Point: self._validate_coord(coord.coordinate) index = _nearest_location(self._locations, coord.coordinate, self._inverted) outside = self._orientation * self._outside(coord.coordinate) - region = "u" if outside < 0 else "d" if outside > 0 else "" + region = 'u' if outside < 0 else 'd' if outside > 0 else '' point = self._loci[index].to_position(coord) return Point( @@ -218,11 +218,11 @@ def to_coordinate(self, point: Point) -> Coord: ) self._validate_point(index, point.position, point.offset, point.region) - if point.region == "u": + if point.region == 'u': if self._inverted: return Coord(self._locations[-1][1] - point.offset - 1) return Coord(self._locations[0][0] + point.offset) - if point.region == "d": + if point.region == 'd': if self._inverted: return Coord(self._locations[0][0] - point.offset) return Coord(self._locations[-1][1] + point.offset - 1) @@ -236,7 +236,7 @@ def to_coordinate(self, point: Point) -> Coord: ) except ValueError as error: - if "Position" in str(error): + if 'Position' in str(error): raise ValueError( str(error).replace( str(point.position - self._offsets[index]), str(point.position) @@ -244,8 +244,8 @@ def to_coordinate(self, point: Point) -> Coord: ) from error raise except IndexError as error: - if "Position" in str(error): + if 'Position' in str(error): raise IndexError( - f"Position {point.position} exceeds multi locus length." + f'Position {point.position} exceeds multi locus length.' ) from error raise From e4b185940e43459dd1d82a03cec38e0023a9e1d7 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 2 Sep 2026 09:05:48 +0200 Subject: [PATCH 207/236] Use '' in string in tests. --- tests/test_multi_locus.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index c3379c7..42c12f3 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -103,10 +103,10 @@ def test_invalid_Point_initialization(): assert str(error.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as error: Point(position=0, offset=0, region='*') - assert str(error.value) == "Region * is invalid, it must be a string from '', 'u' or 'd'." + assert str(error.value) == 'Region * is invalid, it must be a string from "", "u" or "d".' with pytest.raises(ValueError) as error: Point(position=0, offset=0, region=None) - assert str(error.value) == "Region None is invalid, it must be a string from '', 'u' or 'd'." + assert str(error.value) == 'Region None is invalid, it must be a string from "", "u" or "d".' with pytest.raises(ValueError) as error: Point(position='11', offset=0, region='u') assert str(error.value) == 'Value must be an integer.' From 7d99daf937775e063ad636cbf9852a32eb79e834 Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Wed, 2 Sep 2026 10:24:10 +0200 Subject: [PATCH 208/236] Remove redundant object inheritance. --- mutalyzer_crossmapper/crossmapper.py | 2 +- mutalyzer_crossmapper/locus.py | 2 +- mutalyzer_crossmapper/multi_locus.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 0f04a4b..4a54c0f 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -18,7 +18,7 @@ def __str__(self) -> str: return f'{self.position}' -class Genomic(object): +class Genomic(): """Genomic crossmap object.""" def coordinate_to_genomic(self, coord: Coord, length: int | None = None) -> GenomicPoint: """Convert a coordinate dataclass to a genomic point dataclass (g./m./o.). diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index d25c4ad..6c62399 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -46,7 +46,7 @@ def _check_locus(locus: tuple[int, int]) -> None: raise ValueError(f'Locus start {locus[0]} must be smaller than locus end {locus[1]}.') -class Locus(object): +class Locus(): """Locus object.""" def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 6384eb1..0ace68b 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -56,7 +56,7 @@ def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: return [0] + list(accumulate(map(lambda x: x[1] - x[0], locations[::orientation][:-1]))) -class MultiLocus(object): +class MultiLocus(): """MultiLocus object.""" def __init__( From 193373b157069fecac9ed3bec1c904c8d265223e Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Wed, 2 Sep 2026 10:39:27 +0200 Subject: [PATCH 209/236] Add explicit exception chaining. --- mutalyzer_crossmapper/crossmapper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 4a54c0f..1ad536d 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -117,11 +117,11 @@ def noncoding_to_coordinate(self, point: NonCodingPoint) -> Coord: ) except ValueError as error: if 'Position' in str(error): - raise ValueError(str(error).replace(str(point.position - 1), str(point.position))) + raise ValueError(str(error).replace(str(point.position - 1), str(point.position))) from error raise error except IndexError as error: if 'Position' in str(error): - raise IndexError(str(error).replace(str(point.position - 1), str(point.position))) + raise IndexError(str(error).replace(str(point.position - 1), str(point.position))) from error raise error @@ -328,7 +328,7 @@ def _coding_to_coordinate(self, point: CodingPoint) -> Coord: ) except ValueError as error: if 'Position' in str(error): - raise ValueError(str(error).replace(str(position), str(point.position))) + raise ValueError(str(error).replace(str(position), str(point.position))) from error raise def coding_to_coordinate(self, point: CodingPoint) -> Coord: From 0880ed4f6b7aa373e53d38d67d06758b5d656de9 Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Fri, 4 Sep 2026 09:36:28 +0200 Subject: [PATCH 210/236] Allow half-open location ends to equal reference length. --- mutalyzer_crossmapper/crossmapper.py | 12 +++++-- mutalyzer_crossmapper/multi_locus.py | 18 +++++++--- tests/test_crossmapper.py | 53 ++++++++++++++++++++++------ tests/test_multi_locus.py | 24 ++++++++++--- 4 files changed, 85 insertions(+), 22 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 1ad536d..c3f54b0 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -1,6 +1,12 @@ from dataclasses import dataclass -from .multi_locus import MultiLocus, Point, _check_in_range, _check_multi_locus +from .multi_locus import ( + MultiLocus, + Point, + _check_coordinate_within_length, + _check_location_end_within_length, + _check_multi_locus, +) from .locus import Coord, _check_locus, _check_int from .location import _nearest_location @@ -29,7 +35,7 @@ def coordinate_to_genomic(self, coord: Coord, length: int | None = None) -> Geno :returns GenomicPoint: Genomic point dataclass. """ if length is not None: - _check_in_range(coord.coordinate, length) + _check_coordinate_within_length(coord.coordinate, length) return GenomicPoint(coord.coordinate + 1) def genomic_to_coordinate(self, point: GenomicPoint) -> Coord: @@ -199,7 +205,7 @@ def _check_cds( """Check if the CDS is valid.""" _check_locus(cds) if length is not None: - _check_in_range(cds[1], length) + _check_location_end_within_length(cds[1], length) for coord in cds: index = _nearest_location(locations, coord) if coord < locations[index][0] or coord > locations[index][1]: diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 0ace68b..c3d2ba1 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -21,11 +21,19 @@ def __post_init__(self) -> None: ) -def _check_in_range(value: int, length: int) -> None: - """Check if the value no larger than length.""" +def _check_coordinate_within_length(value: int, length: int) -> None: + """Check if a zero-based coordinate is within reference length.""" if value >= length: raise ValueError( - f'Location {value} must be within the bounds of the reference length {length}.' + f'Coordinate {value} is not within the bounds of the reference length {length}.' + ) + + +def _check_location_end_within_length(value: int, length: int) -> None: + """Check if a half-open interval end is within reference length.""" + if value > length: + raise ValueError( + f'Location end {value} is inconsistent with reference length {length}.' ) @@ -42,7 +50,7 @@ def _check_multi_locus(locations: list[tuple[int, int]], length: int | None = No raise ValueError(f'Locus {l2} and locus {l1} are overlapping.') if length is not None: - _check_in_range(locations[-1][1], length) + _check_location_end_within_length(locations[-1][1], length) def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: @@ -84,7 +92,7 @@ def __init__( def _validate_coord(self, coordinate: int) -> None: """Check if the coordinate is valid.""" if self._length is not None: - _check_in_range(coordinate, self._length) + _check_coordinate_within_length(coordinate, self._length) def _validate_point(self, index: int, position: int, offset: int, region: str) -> None: """Validate if a multi locus Point is valid under HGVS rules. diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 4aa1fea..6468651 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -46,7 +46,7 @@ def test_Genomic_invalid_with_length(): assert str(error.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as error: crossmap.coordinate_to_genomic(Coord(99), 99) - assert str(error.value) == 'Location 99 must be within the bounds of the reference length 99.' + assert str(error.value) == 'Coordinate 99 is not within the bounds of the reference length 99.' def test_Genomic_with_length(): @@ -108,7 +108,7 @@ def test_NonCoding_invalid(): assert str(error.value) == 'Value must be an integer.' with pytest.raises(ValueError) as error: NonCoding(_exons, length=70) - assert str(error.value) == 'Location 72 must be within the bounds of the reference length 70.' + assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' # Reverse orientation with pytest.raises(ValueError) as error: @@ -125,19 +125,19 @@ def test_NonCoding_invalid(): assert str(error.value) == 'Value must be an integer.' with pytest.raises(ValueError) as error: NonCoding(_exons, length=70, inverted=True) - assert str(error.value) == 'Location 72 must be within the bounds of the reference length 70.' + assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' def test_NonCoding_invalid_with_length(): """Raise ValueError if coordinate is out of bounds.""" with pytest.raises(ValueError) as error: NonCoding(_exons, length=70) - assert str(error.value) == 'Location 72 must be within the bounds of the reference length 70.' + assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' # Reverse orientation with pytest.raises(ValueError) as error: NonCoding(_exons, length=70, inverted=True) - assert str(error.value) == 'Location 72 must be within the bounds of the reference length 70.' + assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' def test_NonCoding(): @@ -226,7 +226,7 @@ def test_NonCoding_with_length(): ) with pytest.raises(ValueError) as error: crossmap.coordinate_to_noncoding(Coord(75)) - assert str(error.value) == 'Location 75 must be within the bounds of the reference length 75.' + assert str(error.value) == 'Coordinate 75 is not within the bounds of the reference length 75.' with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=4, region='d')) assert str(error.value) == 'Offset 4 exceeds downstream region.' @@ -272,7 +272,7 @@ def test_NonCoding_inverted_with_length(): # Boundary between upstream and sequence end. with pytest.raises(ValueError) as error: crossmap.coordinate_to_noncoding(Coord(75)) - assert str(error.value) == 'Location 75 must be within the bounds of the reference length 75.' + assert str(error.value) == 'Coordinate 75 is not within the bounds of the reference length 75.' with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-4, region='u')) assert str(error.value) == 'Offset -4 exceeds upstream region.' @@ -428,6 +428,22 @@ def test_NonCoding_invalid_offset_inverted(): assert error.value.args[0] == 'Offset -1 at downstream boundary should be positive.' +def test_NonCoding_location_end_equal_reference_length(): + """Half-open exon end may equal reference length.""" + crossmap = NonCoding([(0, 10)], length=10) + + invariant( + crossmap.coordinate_to_noncoding, + Coord(9), + crossmap.noncoding_to_coordinate, + NonCodingPoint(position=10, offset=0, region=''), + ) + + with pytest.raises(ValueError) as error: + crossmap.coordinate_to_noncoding(Coord(10)) + assert str(error.value) == 'Coordinate 10 is not within the bounds of the reference length 10.' + + def test_CodingPoint_invalid_initialization(): """Raise error with invalid initialization.""" with pytest.raises(ValueError) as error: @@ -491,11 +507,11 @@ def test_Coding_invalid_with_length(): """Raise ValueError if coordinate is out of bounds.""" with pytest.raises(ValueError) as error: Coding(_exons, _cds, length=70) - assert str(error.value) == 'Location 72 must be within the bounds of the reference length 70.' + assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' # Reverse orientation with pytest.raises(ValueError) as error: Coding(_exons, _cds, length=70, inverted=True) - assert str(error.value) == 'Location 72 must be within the bounds of the reference length 70.' + assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' def test_Coding(): @@ -627,6 +643,7 @@ def test_Coding_with_length(): ) with pytest.raises(ValueError) as error: crossmap.coordinate_to_coding(Coord(75)) + assert str(error.value) == 'Coordinate 75 is not within the bounds of the reference length 75.' with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=5, offset=4, region='d')) @@ -699,7 +716,7 @@ def test_Coding_inverted_with_length(): # Boundary between upstream and sequence end. with pytest.raises(ValueError) as error: crossmap.coordinate_to_coding(Coord(75)) - assert str(error.value) == 'Location 75 must be within the bounds of the reference length 75.' + assert str(error.value) == 'Coordinate 75 is not within the bounds of the reference length 75.' with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=5, offset=-4, region='u')) assert str(error.value) == 'Offset -4 exceeds upstream region.' @@ -1569,3 +1586,19 @@ def test_Coding_inverted_protein(): crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=-1, region='u', position_in_codon=2) ) + + +def test_Coding_cds_end_equal_reference_length(): + """Half-open exon/CDS end may equal reference length.""" + crossmap = Coding([(0, 10)], (0, 10), length=10) + + invariant( + crossmap.coordinate_to_coding, + Coord(9), + crossmap.coding_to_coordinate, + CodingPoint(position=10, offset=0, region=''), + ) + + with pytest.raises(ValueError) as error: + crossmap.coordinate_to_coding(Coord(10)) + assert str(error.value) == 'Coordinate 10 is not within the bounds of the reference length 10.' diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index 42c12f3..1037410 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -52,8 +52,8 @@ def test_invalid_MultiLocus_initialization(): MultiLocus([(10, 20), (15, 25)]) assert str(error.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' with pytest.raises(ValueError) as error: - MultiLocus([(10, 12), (15, 25)], 25) - assert str(error.value) == 'Location 25 must be within the bounds of the reference length 25.' + MultiLocus([(10, 12), (15, 25)], 24) + assert str(error.value) == 'Location end 25 is inconsistent with reference length 24.' # Inverted MultiLocus initialization with pytest.raises(ValueError) as error: @@ -78,8 +78,8 @@ def test_invalid_MultiLocus_initialization(): MultiLocus([(10, 20), (15, 25)], 25, inverted=True) assert str(error.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' with pytest.raises(ValueError) as error: - MultiLocus([(10, 12), (15, 25)], 25, inverted=True) - assert str(error.value) == 'Location 25 must be within the bounds of the reference length 25.' + MultiLocus([(10, 12), (15, 25)], 24, inverted=True) + assert str(error.value) == 'Location end 25 is inconsistent with reference length 24.' def test_MultiLocus_invalid_coordinate(): @@ -700,3 +700,19 @@ def test_downstream_invalid_offset_inverted(): with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=6, region='d')) assert str(error.value) == 'Offset 6 exceeds downstream region.' + + +def test_MultiLocus_location_end_equal_reference_length(): + """Half-open location end may equal reference length.""" + multi_locus = MultiLocus([(0, 10)], length=10) + + invariant( + multi_locus.to_position, + Coord(9), + multi_locus.to_coordinate, + Point(position=9, offset=0, region=''), + ) + + with pytest.raises(ValueError) as error: + multi_locus.to_position(Coord(10)) + assert str(error.value) == 'Coordinate 10 is not within the bounds of the reference length 10.' From 8eed9dc894b67e697c946569e0b11d02445d9ded Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Fri, 4 Sep 2026 10:22:56 +0200 Subject: [PATCH 211/236] Validate length as a positive integer only. --- mutalyzer_crossmapper/crossmapper.py | 3 ++- mutalyzer_crossmapper/locus.py | 9 ++++++++- mutalyzer_crossmapper/multi_locus.py | 3 ++- tests/test_crossmapper.py | 17 +++++++++++++++++ tests/test_locus.py | 6 ++++++ tests/test_multi_locus.py | 10 ++++++++++ 6 files changed, 45 insertions(+), 3 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index c3f54b0..5629ffc 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -7,7 +7,7 @@ _check_location_end_within_length, _check_multi_locus, ) -from .locus import Coord, _check_locus, _check_int +from .locus import Coord, _check_locus, _check_int, _check_positive_int from .location import _nearest_location @dataclass(slots=True) @@ -35,6 +35,7 @@ def coordinate_to_genomic(self, coord: Coord, length: int | None = None) -> Geno :returns GenomicPoint: Genomic point dataclass. """ if length is not None: + _check_positive_int(length) _check_coordinate_within_length(coord.coordinate, length) return GenomicPoint(coord.coordinate + 1) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 6c62399..7eedc04 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -23,7 +23,7 @@ def __post_init__(self) -> None: def _check_int(value: int) -> None: """Check if the input value type is integer.""" - if not isinstance(value, int): + if not isinstance(value, int) or isinstance(value, bool): raise ValueError('Value must be an integer.') @@ -34,6 +34,13 @@ def _check_non_negative_int(value: int) -> None: raise ValueError('Value must be non-negative.') +def _check_positive_int(value: int) -> None: + """Check if the value is a positive integer.""" + _check_int(value) + if value < 1: + raise ValueError(f'Value {value} is not positive.') + + def _check_locus(locus: tuple[int, int]) -> None: """Check if the locus location is valid.""" if not isinstance(locus, tuple) or len(locus) != 2: diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index c3d2ba1..ecffb70 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from .location import _nearest_location -from .locus import Locus, Coord, _check_locus +from .locus import Locus, Coord, _check_locus, _check_positive_int from .locus import Point as LocusPoint @@ -50,6 +50,7 @@ def _check_multi_locus(locations: list[tuple[int, int]], length: int | None = No raise ValueError(f'Locus {l2} and locus {l1} are overlapping.') if length is not None: + _check_positive_int(length) _check_location_end_within_length(locations[-1][1], length) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 6468651..60ad4b7 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -47,6 +47,15 @@ def test_Genomic_invalid_with_length(): with pytest.raises(ValueError) as error: crossmap.coordinate_to_genomic(Coord(99), 99) assert str(error.value) == 'Coordinate 99 is not within the bounds of the reference length 99.' + with pytest.raises(ValueError) as error: + crossmap.coordinate_to_genomic(Coord(0), 0) + assert str(error.value) == 'Value 0 is not positive.' + with pytest.raises(ValueError) as error: + crossmap.coordinate_to_genomic(Coord(0), -1) + assert str(error.value) == 'Value -1 is not positive.' + with pytest.raises(ValueError) as error: + crossmap.coordinate_to_genomic(Coord(0), '99') + assert str(error.value) == 'Value must be an integer.' def test_Genomic_with_length(): @@ -139,6 +148,10 @@ def test_NonCoding_invalid_with_length(): NonCoding(_exons, length=70, inverted=True) assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' + with pytest.raises(ValueError) as error: + NonCoding(_exons, length=0) + assert str(error.value) == 'Value 0 is not positive.' + def test_NonCoding(): """Forward oriented noncoding transcript.""" @@ -513,6 +526,10 @@ def test_Coding_invalid_with_length(): Coding(_exons, _cds, length=70, inverted=True) assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' + with pytest.raises(ValueError) as error: + Coding(_exons, _cds, length=0) + assert str(error.value) == 'Value 0 is not positive.' + def test_Coding(): """Forward oriented coding transcript.""" diff --git a/tests/test_locus.py b/tests/test_locus.py index 5667212..93c4609 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -24,6 +24,9 @@ def test_invalid_locus_initialization(): with pytest.raises(ValueError) as error: Locus(('10', '20')) assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: + Locus((10, True)) + assert str(error.value) == 'Value must be an integer.' with pytest.raises(ValueError) as error: Locus((10, 10)) assert str(error.value) == 'Locus start 10 must be smaller than locus end 10.' @@ -69,6 +72,9 @@ def test_invalid_coord_initialization(): with pytest.raises(ValueError) as error: Coord([10]) assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: + Coord(True) + assert str(error.value) == 'Value must be an integer.' def test_invalid_locus_point(): diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index 1037410..c35c08c 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -54,6 +54,16 @@ def test_invalid_MultiLocus_initialization(): with pytest.raises(ValueError) as error: MultiLocus([(10, 12), (15, 25)], 24) assert str(error.value) == 'Location end 25 is inconsistent with reference length 24.' + with pytest.raises(ValueError) as error: + MultiLocus([(10, 12), (15, 25)], 0) + assert str(error.value) == 'Value 0 is not positive.' + with pytest.raises(ValueError) as error: + MultiLocus([(10, 12), (15, 25)], '25') + assert str(error.value) == 'Value must be an integer.' + # A bool would otherwise pass as a length, since bool subclasses int. + with pytest.raises(ValueError) as error: + MultiLocus([(10, 12), (15, 25)], True) + assert str(error.value) == 'Value must be an integer.' # Inverted MultiLocus initialization with pytest.raises(ValueError) as error: From 25dda810ba5d8d07bf37f8c4c51768555f1f80fb Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Fri, 4 Sep 2026 11:48:29 +0200 Subject: [PATCH 212/236] Fix downstream boundary validation when the 3' UTR is missing. --- mutalyzer_crossmapper/crossmapper.py | 8 +++++- tests/test_crossmapper.py | 38 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 5629ffc..dd75d3d 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -231,7 +231,13 @@ def _validate_point(self, position: int, region: str) -> None: if position not in range(1, self._exons[1] - self._coding[1] + 1): raise ValueError(f'Position {position} exceeds * region.') if region == 'd': - if position not in (1, self._coding[0], self._exons[1] - self._coding[1]): + # Downstream positions anchor at the 3' UTR boundary, or at the + # last coding position when the 3' UTR is absent. + if self._exons[1] == self._coding[1]: + downstream_boundary = self._coding[1] - self._coding[0] + else: + downstream_boundary = self._exons[1] - self._coding[1] + if position not in (1, downstream_boundary): raise ValueError(f'Position {position} is not in downstream boundary.') diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 60ad4b7..b8af4dd 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -923,6 +923,24 @@ def test_Coding_no_utr3(): ) +def test_Coding_no_utr3_unequal_utr5(): + """Without a 3' UTR the downstream anchor is the last coding position.""" + crossmap = Coding([(10, 20)], (12, 20)) + # Direct transition from CDS to downstream. + invariant( + crossmap.coordinate_to_coding, + Coord(19), + crossmap.coding_to_coordinate, + CodingPoint(position=8, offset=0, region=''), + ) + invariant( + crossmap.coordinate_to_coding, + Coord(20), + crossmap.coding_to_coordinate, + CodingPoint(position=8, offset=1, region='d'), + ) + + def test_Coding_no_utr3_inverted(): """A 3' UTR may be missing.""" crossmap = Coding([(10, 20)], (10, 15), inverted=True) @@ -941,6 +959,26 @@ def test_Coding_no_utr3_inverted(): CodingPoint(position=5, offset=1, region='d'), ) + +def test_Coding_no_utr3_inverted_unequal_utr5(): + """Without a 3' UTR the downstream anchor is the last coding position.""" + crossmap = Coding([(10, 20)], (10, 18), inverted=True) + + # Direct transition from CDS to downstream. + invariant( + crossmap.coordinate_to_coding, + Coord(10), + crossmap.coding_to_coordinate, + CodingPoint(position=8, offset=0, region=''), + ) + invariant( + crossmap.coordinate_to_coding, + Coord(9), + crossmap.coding_to_coordinate, + CodingPoint(position=8, offset=1, region='d'), + ) + + def test_Coding_small_utr5(): """A 5' UTR may be of length one.""" crossmap = Coding([(10, 20)], (11, 15)) From 3f06bbf02e2d1f9ff81db77472af563b24e6812c Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Fri, 4 Sep 2026 11:58:08 +0200 Subject: [PATCH 213/236] Export Coord and Point from the package root. --- mutalyzer_crossmapper/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mutalyzer_crossmapper/__init__.py b/mutalyzer_crossmapper/__init__.py index db521b2..a309a2f 100644 --- a/mutalyzer_crossmapper/__init__.py +++ b/mutalyzer_crossmapper/__init__.py @@ -2,8 +2,8 @@ from .crossmapper import Coding, Genomic, NonCoding, GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint from .location import _nearest_location -from .locus import Locus -from .multi_locus import MultiLocus +from .locus import Coord, Locus +from .multi_locus import MultiLocus, Point def _get_metadata(name: str) -> str: From 4976c30a3e1b47340ce1ef620995bc5f1741d249 Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Fri, 4 Sep 2026 12:04:29 +0200 Subject: [PATCH 214/236] Drop the unused pytest-pep8 configuration and dependency. --- setup.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index c58c160..0b187ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,11 +22,9 @@ python_requires = >=3.10 [options.extras_require] tests = pytest-cov>=2.10.0 - pytest-pep8>=1.0.6 pytest>=5.4.3 [tool:pytest] -pep8ignore = docs/conf.py ALL [coverage:run] source = mutalyzer_crossmapper From 6b2b7924f9cbc6f319772f79a2a24854fd2fe4d2 Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Fri, 4 Sep 2026 16:55:04 +0200 Subject: [PATCH 215/236] Swaph the length and inverted parameters. --- mutalyzer_crossmapper/crossmapper.py | 14 +++++----- mutalyzer_crossmapper/multi_locus.py | 6 +++-- tests/test_crossmapper.py | 20 +++++++------- tests/test_multi_locus.py | 39 +++++++++++++++------------- 4 files changed, 42 insertions(+), 37 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index dd75d3d..d2ac015 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -81,17 +81,17 @@ class NonCoding(Genomic): def __init__( self, locations: list[tuple[int, int]], - length: int | None = None, inverted: bool = False, + length: int | None = None, ) -> None: """ :arg list locations: List of locus locations. - :arg int|None length: Length of the reference sequence. :arg bool inverted: Orientation. + :arg int|None length: Length of the reference sequence. """ _check_multi_locus(locations, length) self._inverted = inverted - self._noncoding = MultiLocus(locations, length, inverted) + self._noncoding = MultiLocus(locations, inverted=inverted, length=length) def coordinate_to_noncoding(self, coord: Coord) -> NonCodingPoint: """Convert a coordinate dataclass to a noncoding point dataclass (n./r.). @@ -161,16 +161,16 @@ def __init__( self, locations: list[tuple[int, int]], cds: tuple[int, int], - length: int|None = None, - inverted: bool = False + inverted: bool = False, + length: int|None = None ) -> None: """ :arg list locations: List of locus locations. :arg tuple cds: Locus location. - :arg int|None length: Length of the reference sequence. :arg bool inverted: Orientation. + :arg int|None length: Length of the reference sequence. """ - NonCoding.__init__(self, locations, length, inverted) + NonCoding.__init__(self, locations, inverted=inverted, length=length) self._check_cds(cds, locations, length) cds_start = self._noncoding.to_position(Coord(cds[0])) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index ecffb70..232c01f 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -71,15 +71,17 @@ class MultiLocus(): def __init__( self, locations: list[tuple[int, int]], - length: int | None = None, inverted: bool = False, + length: int | None = None, ) -> None: """ :arg list locations: List of locus locations. - :arg int|None length: Length of the reference sequence, None if unknown. :arg bool inverted: Orientation. + :arg int|None length: Length of the reference sequence, None if unknown. """ _check_multi_locus(locations, length) + if not isinstance(inverted, bool): + raise ValueError(f'Value {inverted} is not a boolean.') self._locations = locations self._inverted = inverted self._length = length diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index b8af4dd..a90d81a 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -133,7 +133,7 @@ def test_NonCoding_invalid(): NonCoding([(None, 20), (30, None)], inverted=True) assert str(error.value) == 'Value must be an integer.' with pytest.raises(ValueError) as error: - NonCoding(_exons, length=70, inverted=True) + NonCoding(_exons, inverted=True, length=70) assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' @@ -145,7 +145,7 @@ def test_NonCoding_invalid_with_length(): # Reverse orientation with pytest.raises(ValueError) as error: - NonCoding(_exons, length=70, inverted=True) + NonCoding(_exons, inverted=True, length=70) assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' with pytest.raises(ValueError) as error: @@ -247,7 +247,7 @@ def test_NonCoding_with_length(): def test_NonCoding_inverted(): """Reverse oriented noncoding transcript.""" - crossmap = NonCoding(_exons, inverted=True) + crossmap = NonCoding(_exons, True) # Boundary between upstream and transcript. invariant( @@ -280,7 +280,7 @@ def test_NonCoding_inverted(): def test_NonCoding_inverted_with_length(): """Reverse oriented noncoding transcript.""" - crossmap = NonCoding(_exons, length=75, inverted=True) + crossmap = NonCoding(_exons, True, 75) # Boundary between upstream and sequence end. with pytest.raises(ValueError) as error: @@ -347,7 +347,7 @@ def test_NonCoding_invalid_position(): def test_NonCoding_invalid_position_inverted(): """Raise error if position is not valid under HGVS rules.""" - crossmap = NonCoding(_exons, length=75, inverted=True) + crossmap = NonCoding(_exons, inverted=True, length=75) with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=0, offset=1, region='u')) assert str(error.value) == 'Position 0 must be a positive integer.' @@ -405,7 +405,7 @@ def test_NonCoding_invalid_offset(): def test_NonCoding_invalid_offset_inverted(): """Raise error if offset is not valid under HGVS rules.""" - crossmap = NonCoding(_exons, length=75, inverted=True) + crossmap = NonCoding(_exons, inverted=True, length=75) with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=0, region='u')) assert error.value.args[0] == 'Offset 0 at upstream boundary should be negative.' @@ -523,7 +523,7 @@ def test_Coding_invalid_with_length(): assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' # Reverse orientation with pytest.raises(ValueError) as error: - Coding(_exons, _cds, length=70, inverted=True) + Coding(_exons, _cds, inverted=True, length=70) assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' with pytest.raises(ValueError) as error: @@ -667,7 +667,7 @@ def test_Coding_with_length(): def test_Coding_inverted(): """Reverse oriented coding transcript.""" - crossmap = Coding(_exons, _cds, inverted=True) + crossmap = Coding(_exons, _cds, True) # Boundary between upstream and 5' UTR. invariant( @@ -728,7 +728,7 @@ def test_Coding_inverted(): def test_Coding_inverted_with_length(): """Reverse oriented coding transcript.""" - crossmap = Coding(_exons, _cds, length=75, inverted=True) + crossmap = Coding(_exons, _cds, True, 75) # Boundary between upstream and sequence end. with pytest.raises(ValueError) as error: @@ -1452,7 +1452,7 @@ def test_Coding_invalid_offset(): def test_Coding_invalid_offset_inverted(): """Raise error if offset in coding point is invalid.""" - crossmap = Coding(_exons, _cds, length=75, inverted=True) + crossmap = Coding(_exons, _cds, inverted=True, length=75) with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-6, region='u')) diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index c35c08c..6be2afa 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -52,18 +52,21 @@ def test_invalid_MultiLocus_initialization(): MultiLocus([(10, 20), (15, 25)]) assert str(error.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' with pytest.raises(ValueError) as error: - MultiLocus([(10, 12), (15, 25)], 24) + MultiLocus([(10, 12), (15, 25)], length=24) assert str(error.value) == 'Location end 25 is inconsistent with reference length 24.' with pytest.raises(ValueError) as error: - MultiLocus([(10, 12), (15, 25)], 0) + MultiLocus([(10, 12), (15, 25)], length=0) assert str(error.value) == 'Value 0 is not positive.' with pytest.raises(ValueError) as error: - MultiLocus([(10, 12), (15, 25)], '25') + MultiLocus([(10, 12), (15, 25)], length='25') assert str(error.value) == 'Value must be an integer.' # A bool would otherwise pass as a length, since bool subclasses int. with pytest.raises(ValueError) as error: - MultiLocus([(10, 12), (15, 25)], True) + MultiLocus([(10, 12), (15, 25)], length=True) assert str(error.value) == 'Value must be an integer.' + with pytest.raises(ValueError) as error: + MultiLocus([(10, 12), (15, 25)], 100) + assert str(error.value) == 'Value 100 is not a boolean.' # Inverted MultiLocus initialization with pytest.raises(ValueError) as error: @@ -85,10 +88,10 @@ def test_invalid_MultiLocus_initialization(): MultiLocus([('10', '20'), (30, 40)], inverted=True) assert str(error.value) == 'Value must be an integer.' with pytest.raises(ValueError) as error: - MultiLocus([(10, 20), (15, 25)], 25, inverted=True) + MultiLocus([(10, 20), (15, 25)], inverted=True, length=25) assert str(error.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' with pytest.raises(ValueError) as error: - MultiLocus([(10, 12), (15, 25)], 24, inverted=True) + MultiLocus([(10, 12), (15, 25)], inverted=True, length=24) assert str(error.value) == 'Location end 25 is inconsistent with reference length 24.' @@ -196,7 +199,7 @@ def test_MultiLocus(): def test_MultiLocus_inverted(): """Reverse oriented MultiLocus.""" - multi_locus = MultiLocus(_locations, None, True) + multi_locus = MultiLocus(_locations, True) # Boundary between upstream and the first locus. invariant( @@ -297,7 +300,7 @@ def test_MultiLocus_with_length(): def test_MultiLocus_inverted_with_length(): """Inverted MultiLocus with length.""" - multi_locus = MultiLocus(_locations, length=74, inverted=True) + multi_locus = MultiLocus(_locations, True, 74) # Boundary between the first locus and upstream. invariant( @@ -345,7 +348,7 @@ def test_MultiLocus_adjacent_loci(): def test_MultiLocus_adjacent_loci_inverted(): """Positions are continuous when loci are adjacent.""" - multi_locus = MultiLocus([(1, 3), (3, 5)], None, True) + multi_locus = MultiLocus([(1, 3), (3, 5)], inverted=True) invariant( multi_locus.to_position, @@ -381,7 +384,7 @@ def test_MultiLocus_offsets_odd(): def test_MultiLocus_offsets_odd_inverted(): """Offets exacly between two loci are assigned to the upstream locus.""" - multi_locus = MultiLocus([(1, 3), (6, 8)], None, True) + multi_locus = MultiLocus([(1, 3), (6, 8)], inverted=True) invariant( multi_locus.to_position, Coord(4), @@ -416,7 +419,7 @@ def test_MultiLocus_offsets_even(): def test_MultiLocus_offsets_even_inverted(): """Offsets are assigned to the nearest locus.""" - multi_locus = MultiLocus([(1, 3), (7, 9)], None, True) + multi_locus = MultiLocus([(1, 3), (7, 9)], inverted=True) invariant( multi_locus.to_position, @@ -475,7 +478,7 @@ def test_one_base_exon(): def test_one_base_exon_inverted(): """One base exons.""" - multi_locus = MultiLocus([(1, 2), (4, 5)], None, True) + multi_locus = MultiLocus([(1, 2), (4, 5)], inverted=True) invariant( multi_locus.to_position, Coord(0), @@ -528,7 +531,7 @@ def test_upstream_invalid_position(): def test_upstream_invalid_position_inverted(): - multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) + multi_locus = MultiLocus([(5, 10), (15, 20)], inverted=True, length=25) with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=1, offset=-1, region='u')) assert str(error.value) == 'Position 1 is not at upstream boundary.' @@ -554,7 +557,7 @@ def test_upstream_invalid_offset(): def test_upstream_invalid_offset_inverted(): - multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) + multi_locus = MultiLocus([(5, 10), (15, 20)], inverted=True, length=25) with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=1, region='u')) assert str(error.value) == 'Offset 1 at upstream region should be negative.' @@ -580,7 +583,7 @@ def test_transcribed_invalid_position(): def test_transcribed_invalid_position_inverted(): - multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) + multi_locus = MultiLocus([(5, 10), (15, 20)], inverted=True, length=25) with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=-1, offset=0, region='')) assert str(error.value) == 'Value must be non-negative.' @@ -621,7 +624,7 @@ def test_transcribed_invalid_offset(): def test_transcribed_invalid_offset_inverted(): - multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) + multi_locus = MultiLocus([(5, 10), (15, 20)], inverted=True, length=25) with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=5, region='')) assert str(error.value) == 'Offset 5 at the last locus should be in the downstream region.' @@ -665,7 +668,7 @@ def test_downstream_invalid_position(): def test_downstream_invalid_position_inverted(): - multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) + multi_locus = MultiLocus([(5, 10), (15, 20)], inverted=True, length=25) with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=1, region='d')) assert str(error.value) == 'Position 0 is not at downstream boundary.' @@ -697,7 +700,7 @@ def test_downstream_invalid_offset(): def test_downstream_invalid_offset_inverted(): - multi_locus = MultiLocus([(5, 10), (15, 20)], length=25, inverted=True) + multi_locus = MultiLocus([(5, 10), (15, 20)], inverted=True, length=25) with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=9, offset=-1, region='d')) assert str(error.value) == 'Offset -1 at downstream region should be positive.' From 03adc65e0862fb95206945f5726fbe8925ed684f Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Sat, 5 Sep 2026 10:27:52 +0200 Subject: [PATCH 216/236] Correct degenerate also in protein to coordinate. --- mutalyzer_crossmapper/crossmapper.py | 19 ++++------- tests/test_crossmapper.py | 48 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index d2ac015..69d637d 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -412,17 +412,10 @@ def protein_to_coordinate(self, point: ProteinPoint) -> Coord: :returns Coord: Coordinate dataclass. """ if point.region in ('-', 'u'): - return self._coding_to_coordinate( - CodingPoint( - position=3 * point.position - point.position_in_codon + 1, - offset=point.offset, - region=point.region - ) - ) - return self._coding_to_coordinate( - CodingPoint( - position=3 * point.position + point.position_in_codon - 3, - offset=point.offset, - region=point.region - ) + position = 3 * point.position - point.position_in_codon + 1 + else: + position = 3 * point.position + point.position_in_codon - 3 + + return self.coding_to_coordinate( + CodingPoint(position=position, offset=point.offset, region=point.region) ) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index a90d81a..61f8561 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -1643,6 +1643,54 @@ def test_Coding_inverted_protein(): ) +def test_Coding_protein_degenerate(): + """Degenerate upstream and downstream protein positions are corrected.""" + crossmap = Coding([(10, 20)], (12, 18)) + + # Degenerate position in upstream. + degenerate_equal( + crossmap.protein_to_coordinate, + Coord(9), + [ + ProteinPoint(position=1, offset=-1, region='u', position_in_codon=2), + ProteinPoint(position=1, offset=0, region='-', position_in_codon=1), + ], + ) + # Degenerate position in downstream. + degenerate_equal( + crossmap.protein_to_coordinate, + Coord(20), + [ + ProteinPoint(position=1, offset=1, region='d', position_in_codon=2), + ProteinPoint(position=1, offset=0, region='*', position_in_codon=3), + ], + ) + + +def test_Coding_inverted_protein_degenerate(): + """Degenerate upstream and downstream protein positions are corrected.""" + crossmap = Coding([(10, 20)], (12, 18), inverted=True) + + # Degenerate position in upstream. + degenerate_equal( + crossmap.protein_to_coordinate, + Coord(20), + [ + ProteinPoint(position=1, offset=-1, region='u', position_in_codon=2), + ProteinPoint(position=1, offset=0, region='-', position_in_codon=1), + ], + ) + # Degenerate position in downstream. + degenerate_equal( + crossmap.protein_to_coordinate, + Coord(9), + [ + ProteinPoint(position=1, offset=1, region='d', position_in_codon=2), + ProteinPoint(position=1, offset=0, region='*', position_in_codon=3), + ], + ) + + def test_Coding_cds_end_equal_reference_length(): """Half-open exon/CDS end may equal reference length.""" crossmap = Coding([(0, 10)], (0, 10), length=10) From 73d98274fb3912e89f9556f513b9ae1f221a4dde Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Sat, 5 Sep 2026 10:42:41 +0200 Subject: [PATCH 217/236] Distinguish out of order loci from overlapping ones. --- mutalyzer_crossmapper/multi_locus.py | 4 +++- tests/test_crossmapper.py | 4 ++-- tests/test_multi_locus.py | 8 ++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 232c01f..cb2eea0 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -46,8 +46,10 @@ def _check_multi_locus(locations: list[tuple[int, int]], length: int | None = No _check_locus(locus) for l1, l2 in zip(locations, locations[1:]): + if l2[0] < l1[0]: + raise ValueError(f'Locus {l1} and locus {l2} are not in ascending order.') if l2[0] < l1[1]: - raise ValueError(f'Locus {l2} and locus {l1} are overlapping.') + raise ValueError(f'Locus {l1} and locus {l2} are overlapping.') if length is not None: _check_positive_int(length) diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 61f8561..4c0dbe3 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -111,7 +111,7 @@ def test_NonCoding_invalid(): assert str(error.value) == 'Locus must be a tuple of two values.' with pytest.raises(ValueError) as error: NonCoding([(10, 20), (15, 25)]) - assert str(error.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' + assert str(error.value) == 'Locus (10, 20) and locus (15, 25) are overlapping.' with pytest.raises(ValueError) as error: NonCoding([(None, 20), (30, None)]) assert str(error.value) == 'Value must be an integer.' @@ -128,7 +128,7 @@ def test_NonCoding_invalid(): assert str(error.value) == 'Locus must be a tuple of two values.' with pytest.raises(ValueError) as error: NonCoding([(10, 20), (15, 25)], inverted=True) - assert str(error.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' + assert str(error.value) == 'Locus (10, 20) and locus (15, 25) are overlapping.' with pytest.raises(ValueError) as error: NonCoding([(None, 20), (30, None)], inverted=True) assert str(error.value) == 'Value must be an integer.' diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index 6be2afa..3e4abd7 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -50,7 +50,11 @@ def test_invalid_MultiLocus_initialization(): assert str(error.value) == 'Value must be an integer.' with pytest.raises(ValueError) as error: MultiLocus([(10, 20), (15, 25)]) - assert str(error.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' + assert str(error.value) == 'Locus (10, 20) and locus (15, 25) are overlapping.' + # Disjoint, so out of order rather than overlapping. + with pytest.raises(ValueError) as error: + MultiLocus([(30, 40), (10, 20)]) + assert str(error.value) == 'Locus (30, 40) and locus (10, 20) are not in ascending order.' with pytest.raises(ValueError) as error: MultiLocus([(10, 12), (15, 25)], length=24) assert str(error.value) == 'Location end 25 is inconsistent with reference length 24.' @@ -89,7 +93,7 @@ def test_invalid_MultiLocus_initialization(): assert str(error.value) == 'Value must be an integer.' with pytest.raises(ValueError) as error: MultiLocus([(10, 20), (15, 25)], inverted=True, length=25) - assert str(error.value) == 'Locus (15, 25) and locus (10, 20) are overlapping.' + assert str(error.value) == 'Locus (10, 20) and locus (15, 25) are overlapping.' with pytest.raises(ValueError) as error: MultiLocus([(10, 12), (15, 25)], inverted=True, length=24) assert str(error.value) == 'Location end 25 is inconsistent with reference length 24.' From 58f3f0fe6ee8ef4d1514066d6a059874a8f09552 Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Sun, 6 Sep 2026 08:50:00 +0200 Subject: [PATCH 218/236] Remove the Coord dataclass and switch back to plain integers for coordinates. --- docs/api/locus.rst | 3 - docs/library.rst | 2 +- mutalyzer_crossmapper/__init__.py | 2 +- mutalyzer_crossmapper/crossmapper.py | 85 +++--- mutalyzer_crossmapper/locus.py | 47 ++-- mutalyzer_crossmapper/multi_locus.py | 31 +-- tests/helper.py | 2 +- tests/test_crossmapper.py | 370 +++++++++++++-------------- tests/test_locus.py | 44 ++-- tests/test_multi_locus.py | 116 ++++----- 10 files changed, 348 insertions(+), 354 deletions(-) diff --git a/docs/api/locus.rst b/docs/api/locus.rst index 0a98eaa..93aadff 100644 --- a/docs/api/locus.rst +++ b/docs/api/locus.rst @@ -3,6 +3,3 @@ Locus .. automodule:: mutalyzer_crossmapper.locus :members: - -.. autoclass:: mutalyzer_crossmapper.locus.Coord - :members: \ No newline at end of file diff --git a/docs/library.rst b/docs/library.rst index 52901b3..4318611 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -239,7 +239,7 @@ The ``Locus`` Class .. code-block:: python - >>> from mutalyzer_crossmapper.locus import Locus, Point, Coord + >>> from mutalyzer_crossmapper.locus import Locus, Point >>> locus = Locus((10, 20)) >>> locus.to_position(9) Point(position=0, offset=-1) diff --git a/mutalyzer_crossmapper/__init__.py b/mutalyzer_crossmapper/__init__.py index a309a2f..48fb268 100644 --- a/mutalyzer_crossmapper/__init__.py +++ b/mutalyzer_crossmapper/__init__.py @@ -2,7 +2,7 @@ from .crossmapper import Coding, Genomic, NonCoding, GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint from .location import _nearest_location -from .locus import Coord, Locus +from .locus import Locus from .multi_locus import MultiLocus, Point diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 69d637d..55b6996 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -7,7 +7,7 @@ _check_location_end_within_length, _check_multi_locus, ) -from .locus import Coord, _check_locus, _check_int, _check_positive_int +from .locus import _check_locus, _check_int, _check_non_negative_int, _check_positive_int from .location import _nearest_location @dataclass(slots=True) @@ -26,27 +26,28 @@ def __str__(self) -> str: class Genomic(): """Genomic crossmap object.""" - def coordinate_to_genomic(self, coord: Coord, length: int | None = None) -> GenomicPoint: - """Convert a coordinate dataclass to a genomic point dataclass (g./m./o.). + def coordinate_to_genomic(self, coordinate: int, length: int | None = None) -> GenomicPoint: + """Convert a coordinate to a genomic point dataclass (g./m./o.). - :arg Coord coordinate: Coordinate dataclass. + :arg int coordinate: Coordinate. :arg int|None length: Length of the sequence. :returns GenomicPoint: Genomic point dataclass. """ + _check_non_negative_int(coordinate) if length is not None: _check_positive_int(length) - _check_coordinate_within_length(coord.coordinate, length) - return GenomicPoint(coord.coordinate + 1) + _check_coordinate_within_length(coordinate, length) + return GenomicPoint(coordinate + 1) - def genomic_to_coordinate(self, point: GenomicPoint) -> Coord: - """Convert a genomic point dataclass (g./m./o.) to a coordinate dataclass. + def genomic_to_coordinate(self, point: GenomicPoint) -> int: + """Convert a genomic point dataclass (g./m./o.) to a coordinate. :arg GenomicPoint point: Genomic point dataclass. - :returns Coord: Coordinate dataclass. + :returns int: Coordinate. """ - return Coord(point.position - 1) + return point.position - 1 @dataclass(slots=True) @@ -93,26 +94,26 @@ def __init__( self._inverted = inverted self._noncoding = MultiLocus(locations, inverted=inverted, length=length) - def coordinate_to_noncoding(self, coord: Coord) -> NonCodingPoint: - """Convert a coordinate dataclass to a noncoding point dataclass (n./r.). + def coordinate_to_noncoding(self, coordinate: int) -> NonCodingPoint: + """Convert a coordinate to a noncoding point dataclass (n./r.). - :arg Coord coord: Coordinate dataclass. + :arg int coordinate: Coordinate. :returns NonCodingPoint: Noncoding point dataclass. """ - point = self._noncoding.to_position(coord) + point = self._noncoding.to_position(coordinate) return NonCodingPoint( position=point.position + 1, offset=point.offset, region=point.region ) - def noncoding_to_coordinate(self, point: NonCodingPoint) -> Coord: - """Convert a noncoding point dataclass (n./r.) to a coordinate dataclass. + def noncoding_to_coordinate(self, point: NonCodingPoint) -> int: + """Convert a noncoding point dataclass (n./r.) to a coordinate. :arg NonCodingPoint point: Noncoding point dataclass. - :returns Coord: Coordinate dataclass. + :returns int: Coordinate. """ try: return self._noncoding.to_coordinate( @@ -173,10 +174,10 @@ def __init__( NonCoding.__init__(self, locations, inverted=inverted, length=length) self._check_cds(cds, locations, length) - cds_start = self._noncoding.to_position(Coord(cds[0])) - cds_end = self._noncoding.to_position(Coord(cds[1] - 1)) - exon_start = self._noncoding.to_position(Coord(locations[0][0])) - exon_end = self._noncoding.to_position(Coord(locations[-1][1] - 1)) + cds_start = self._noncoding.to_position(cds[0]) + cds_end = self._noncoding.to_position(cds[1] - 1) + exon_start = self._noncoding.to_position(locations[0][0]) + exon_end = self._noncoding.to_position(locations[-1][1] - 1) if self._inverted: self._coding = ( @@ -241,14 +242,14 @@ def _validate_point(self, position: int, region: str) -> None: raise ValueError(f'Position {position} is not in downstream boundary.') - def _coordinate_to_coding(self, coord: Coord) -> CodingPoint: - """Convert a coordinate dataclass to a coding point dataclass (c./r.). + def _coordinate_to_coding(self, coordinate: int) -> CodingPoint: + """Convert a coordinate to a coding point dataclass (c./r.). - :arg Coord coord: Coordinate dataclass. + :arg int coordinate: Coordinate. :returns CodingPoint: Coding point dataclass (c./r.). """ - noncoding_point = self._noncoding.to_position(coord) + noncoding_point = self._noncoding.to_position(coordinate) position = noncoding_point.position offset = noncoding_point.offset @@ -275,15 +276,15 @@ def _coordinate_to_coding(self, coord: Coord) -> CodingPoint: region = '' return CodingPoint(position=position, offset=offset, region=region) - def coordinate_to_coding(self, coord: Coord, degenerate: bool = False) -> CodingPoint: - """Convert a coordinate dataclass to a coding point dataclass (c./r.). + def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> CodingPoint: + """Convert a coordinate to a coding point dataclass (c./r.). - :arg Coord coord: Coordinate dataclass. + :arg int coordinate: Coordinate. :arg bool degenerate: Return a degenerate coding point dataclass. :returns CodingPoint: Coding point dataclass (c./r.). """ - point = self._coordinate_to_coding(coord) + point = self._coordinate_to_coding(coordinate) if not degenerate: return point @@ -303,12 +304,12 @@ def coordinate_to_coding(self, coord: Coord, degenerate: bool = False) -> Coding return CodingPoint(position=position, offset=0, region='*') return point - def _coding_to_coordinate(self, point: CodingPoint) -> Coord: - """Convert a coding point dataclass (c./r.) to a coordinate dataclass. + def _coding_to_coordinate(self, point: CodingPoint) -> int: + """Convert a coding point dataclass (c./r.) to a coordinate. :arg CodingPoint point: Coding point dataclass (c./r.). - :returns Coord: Coordinate dataclass. + :returns int: Coordinate. """ region = point.region position = point.position @@ -344,12 +345,12 @@ def _coding_to_coordinate(self, point: CodingPoint) -> Coord: raise ValueError(str(error).replace(str(position), str(point.position))) from error raise - def coding_to_coordinate(self, point: CodingPoint) -> Coord: - """Convert a coding point dataclass (c./r.) to a coordinate dataclass. + def coding_to_coordinate(self, point: CodingPoint) -> int: + """Convert a coding point dataclass (c./r.) to a coordinate. :arg CodingPoint point: Coding point dataclass (c./r.). - :returns Coord: Coordinate dataclass. + :returns int: Coordinate. """ # Silently correct for degenerate points if point.offset == 0: @@ -380,14 +381,14 @@ def coding_to_coordinate(self, point: CodingPoint) -> Coord: return self._coding_to_coordinate(point) - def coordinate_to_protein(self, coord: Coord) -> ProteinPoint: - """Convert a coordinate dataclass to a protein point dataclass (p.). + def coordinate_to_protein(self, coordinate: int) -> ProteinPoint: + """Convert a coordinate to a protein point dataclass (p.). - :arg Coord coord: Coordinate dataclass. + :arg int coordinate: Coordinate. :returns ProteinPoint: Protein point dataclass (p.). """ - point = self.coordinate_to_coding(coord) + point = self.coordinate_to_coding(coordinate) position = point.position if point.region in ('-', 'u'): @@ -404,12 +405,12 @@ def coordinate_to_protein(self, coord: Coord) -> ProteinPoint: offset=point.offset ) - def protein_to_coordinate(self, point: ProteinPoint) -> Coord: - """Convert a protein point dataclass (p.) to a coordinate dataclass. + def protein_to_coordinate(self, point: ProteinPoint) -> int: + """Convert a protein point dataclass (p.) to a coordinate. :arg ProteinPoint point: Protein point dataclass (p.). - :returns Coord: Coordinate dataclass. + :returns int: Coordinate. """ if point.region in ('-', 'u'): position = 3 * point.position - point.position_in_codon + 1 diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 7eedc04..ce8b26e 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -12,15 +12,6 @@ def __post_init__(self) -> None: _check_int(self.offset) -@dataclass(slots=True) -class Coord: - """Coordinate dataclass.""" - coordinate: int - - def __post_init__(self) -> None: - _check_non_negative_int(self.coordinate) - - def _check_int(value: int) -> None: """Check if the input value type is integer.""" if not isinstance(value, int) or isinstance(value, bool): @@ -79,35 +70,37 @@ def _validate_point(self, position: int, offset: int) -> None: if position > self._end - 1: raise IndexError(f'Position {position} exceeds locus length.') - def to_position(self, coord: Coord) -> Point: - """Convert a coordinate dataclass to a locus point dataclass. + def to_position(self, coordinate: int) -> Point: + """Convert a coordinate to a locus point dataclass. - :arg Coord coord: Coordinate dataclass. + :arg int coordinate: Coordinate. :returns Point: Locus point dataclass. """ + _check_non_negative_int(coordinate) + if self._inverted: - if coord.coordinate > self.boundary[1]: - return Point(position=0, offset=self.boundary[1] - coord.coordinate) - if coord.coordinate < self.boundary[0]: - return Point(position=self._end - 1, offset=self.boundary[0] - coord.coordinate) - return Point(position=self.boundary[1] - coord.coordinate, offset=0) + if coordinate > self.boundary[1]: + return Point(position=0, offset=self.boundary[1] - coordinate) + if coordinate < self.boundary[0]: + return Point(position=self._end - 1, offset=self.boundary[0] - coordinate) + return Point(position=self.boundary[1] - coordinate, offset=0) - if coord.coordinate < self.boundary[0]: - return Point(position=0, offset=coord.coordinate - self.boundary[0]) - if coord.coordinate > self.boundary[1]: - return Point(position=self._end - 1, offset=coord.coordinate - self.boundary[1]) - return Point(position=coord.coordinate - self.boundary[0], offset=0) + if coordinate < self.boundary[0]: + return Point(position=0, offset=coordinate - self.boundary[0]) + if coordinate > self.boundary[1]: + return Point(position=self._end - 1, offset=coordinate - self.boundary[1]) + return Point(position=coordinate - self.boundary[0], offset=0) - def to_coordinate(self, point: Point) -> Coord: - """Convert a locus point dataclass to a coordinate dataclass. + def to_coordinate(self, point: Point) -> int: + """Convert a locus point dataclass to a coordinate. :arg Point point: Locus point dataclass. - :returns Coord: Coordinate dataclass. + :returns int: Coordinate. """ self._validate_point(point.position, point.offset) if self._inverted: - return Coord(self.boundary[1] - point.position - point.offset) - return Coord(self.boundary[0] + point.position + point.offset) + return self.boundary[1] - point.position - point.offset + return self.boundary[0] + point.position + point.offset diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index cb2eea0..53576cd 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from .location import _nearest_location -from .locus import Locus, Coord, _check_locus, _check_positive_int +from .locus import Locus, _check_locus, _check_non_negative_int, _check_positive_int from .locus import Point as LocusPoint @@ -200,18 +200,19 @@ def _outside(self, coordinate: int) -> int: return coordinate - self._loci[-1].boundary[1] return 0 - def to_position(self, coord: Coord) -> Point: - """Convert a coordinate dataclass to a multi locus point dataclass. + def to_position(self, coordinate: int) -> Point: + """Convert a coordinate to a multi locus point dataclass. - :arg Coord coord: Coordinate dataclass. + :arg int coordinate: Coordinate. :returns Point: Multi locus point dataclass. """ - self._validate_coord(coord.coordinate) - index = _nearest_location(self._locations, coord.coordinate, self._inverted) - outside = self._orientation * self._outside(coord.coordinate) + _check_non_negative_int(coordinate) + self._validate_coord(coordinate) + index = _nearest_location(self._locations, coordinate, self._inverted) + outside = self._orientation * self._outside(coordinate) region = 'u' if outside < 0 else 'd' if outside > 0 else '' - point = self._loci[index].to_position(coord) + point = self._loci[index].to_position(coordinate) return Point( position=point.position + self._offsets[self._direction(index)], @@ -219,12 +220,12 @@ def to_position(self, coord: Coord) -> Point: region=region, ) - def to_coordinate(self, point: Point) -> Coord: - """Convert a multi locus point dataclass to a coordinate dataclass. + def to_coordinate(self, point: Point) -> int: + """Convert a multi locus point dataclass to a coordinate. :arg Point point: Multi locus point dataclass. - :returns Coord: Coordinate dataclass. + :returns int: Coordinate. """ index = min( len(self._offsets), max(0, bisect_right(self._offsets, point.position) - 1) @@ -233,12 +234,12 @@ def to_coordinate(self, point: Point) -> Coord: if point.region == 'u': if self._inverted: - return Coord(self._locations[-1][1] - point.offset - 1) - return Coord(self._locations[0][0] + point.offset) + return self._locations[-1][1] - point.offset - 1 + return self._locations[0][0] + point.offset if point.region == 'd': if self._inverted: - return Coord(self._locations[0][0] - point.offset) - return Coord(self._locations[-1][1] + point.offset - 1) + return self._locations[0][0] - point.offset + return self._locations[-1][1] + point.offset - 1 try: return self._loci[self._direction(index)].to_coordinate( diff --git a/tests/helper.py b/tests/helper.py index 386cbbb..59b5b1d 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -6,4 +6,4 @@ def invariant(f, x, f_i, y): def degenerate_equal(f, coordinate, locations): assert f(locations[0]) == coordinate - assert len(set(obj.coordinate for obj in map(f, locations))) == 1 + assert len(set(map(f, locations))) == 1 diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 4c0dbe3..be321a8 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -1,4 +1,4 @@ -from mutalyzer_crossmapper.crossmapper import Coord, Genomic, NonCoding, Coding, GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint +from mutalyzer_crossmapper.crossmapper import Genomic, NonCoding, Coding, GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint from helper import degenerate_equal, invariant import pytest @@ -26,13 +26,13 @@ def test_Genomic(): invariant( crossmap.coordinate_to_genomic, - Coord(0), + 0, crossmap.genomic_to_coordinate, GenomicPoint(position=1), ) invariant( crossmap.coordinate_to_genomic, - Coord(98), + 98, crossmap.genomic_to_coordinate, GenomicPoint(position=99), ) @@ -42,19 +42,19 @@ def test_Genomic_invalid_with_length(): """Raise ValueError if coordinate is out of bounds.""" crossmap = Genomic() with pytest.raises(ValueError) as error: - crossmap.coordinate_to_genomic(Coord(-1), 99) + crossmap.coordinate_to_genomic(-1, 99) assert str(error.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as error: - crossmap.coordinate_to_genomic(Coord(99), 99) + crossmap.coordinate_to_genomic(99, 99) assert str(error.value) == 'Coordinate 99 is not within the bounds of the reference length 99.' with pytest.raises(ValueError) as error: - crossmap.coordinate_to_genomic(Coord(0), 0) + crossmap.coordinate_to_genomic(0, 0) assert str(error.value) == 'Value 0 is not positive.' with pytest.raises(ValueError) as error: - crossmap.coordinate_to_genomic(Coord(0), -1) + crossmap.coordinate_to_genomic(0, -1) assert str(error.value) == 'Value -1 is not positive.' with pytest.raises(ValueError) as error: - crossmap.coordinate_to_genomic(Coord(0), '99') + crossmap.coordinate_to_genomic(0, '99') assert str(error.value) == 'Value must be an integer.' @@ -64,13 +64,13 @@ def test_Genomic_with_length(): invariant( crossmap.coordinate_to_genomic, - (Coord(0), 99), + (0, 99), crossmap.genomic_to_coordinate, GenomicPoint(position=1), ) invariant( crossmap.coordinate_to_genomic, - (Coord(98), 99), + (98, 99), crossmap.genomic_to_coordinate, GenomicPoint(position=99), ) @@ -160,19 +160,19 @@ def test_NonCoding(): # Boundary between upstream and transcript. invariant( crossmap.coordinate_to_noncoding, - Coord(3) , + 3 , crossmap.noncoding_to_coordinate, NonCodingPoint(position=1, offset=-2, region='u'), ) invariant( crossmap.coordinate_to_noncoding, - Coord(4), + 4, crossmap.noncoding_to_coordinate, NonCodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_noncoding, - Coord(5), + 5, crossmap.noncoding_to_coordinate, NonCodingPoint(position=1, offset=0, region=''), ) @@ -180,13 +180,13 @@ def test_NonCoding(): # Boundary between downstream and transcript. invariant( crossmap.coordinate_to_noncoding, - Coord(71), + 71, crossmap.noncoding_to_coordinate, NonCodingPoint(position=22, offset=0, region=''), ) invariant( crossmap.coordinate_to_noncoding, - Coord(72), + 72, crossmap.noncoding_to_coordinate, NonCodingPoint(position=22, offset=1, region='d'), ) @@ -199,19 +199,19 @@ def test_NonCoding_with_length(): # Boundary between upstream and transcript. invariant( crossmap.coordinate_to_noncoding, - Coord(3) , + 3 , crossmap.noncoding_to_coordinate, NonCodingPoint(position=1, offset=-2, region='u'), ) invariant( crossmap.coordinate_to_noncoding, - Coord(4), + 4, crossmap.noncoding_to_coordinate, NonCodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_noncoding, - Coord(5), + 5, crossmap.noncoding_to_coordinate, NonCodingPoint(position=1, offset=0, region=''), ) @@ -219,13 +219,13 @@ def test_NonCoding_with_length(): # Boundary between downstream and transcript. invariant( crossmap.coordinate_to_noncoding, - Coord(71), + 71, crossmap.noncoding_to_coordinate, NonCodingPoint(position=22, offset=0, region=''), ) invariant( crossmap.coordinate_to_noncoding, - Coord(72), + 72, crossmap.noncoding_to_coordinate, NonCodingPoint(position=22, offset=1, region='d'), ) @@ -233,12 +233,12 @@ def test_NonCoding_with_length(): # Boundary between downstream and sequence end. invariant( crossmap.coordinate_to_noncoding, - Coord(74), + 74, crossmap.noncoding_to_coordinate, NonCodingPoint(position=22, offset=3, region='d'), ) with pytest.raises(ValueError) as error: - crossmap.coordinate_to_noncoding(Coord(75)) + crossmap.coordinate_to_noncoding(75) assert str(error.value) == 'Coordinate 75 is not within the bounds of the reference length 75.' with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=4, region='d')) @@ -252,13 +252,13 @@ def test_NonCoding_inverted(): # Boundary between upstream and transcript. invariant( crossmap.coordinate_to_noncoding, - Coord(72), + 72, crossmap.noncoding_to_coordinate, NonCodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_noncoding, - Coord(71), + 71, crossmap.noncoding_to_coordinate, NonCodingPoint(position=1, offset=0, region=''), ) @@ -266,13 +266,13 @@ def test_NonCoding_inverted(): # Boundary between downstream and transcript. invariant( crossmap.coordinate_to_noncoding, - Coord(5), + 5, crossmap.noncoding_to_coordinate, NonCodingPoint(position=22, offset=0, region=''), ) invariant( crossmap.coordinate_to_noncoding, - Coord(4), + 4, crossmap.noncoding_to_coordinate, NonCodingPoint(position=22, offset=1, region='d'), ) @@ -284,14 +284,14 @@ def test_NonCoding_inverted_with_length(): # Boundary between upstream and sequence end. with pytest.raises(ValueError) as error: - crossmap.coordinate_to_noncoding(Coord(75)) + crossmap.coordinate_to_noncoding(75) assert str(error.value) == 'Coordinate 75 is not within the bounds of the reference length 75.' with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-4, region='u')) assert str(error.value) == 'Offset -4 exceeds upstream region.' invariant( crossmap.coordinate_to_noncoding, - Coord(74), + 74, crossmap.noncoding_to_coordinate, NonCodingPoint(position=1, offset=-3, region='u'), ) @@ -299,13 +299,13 @@ def test_NonCoding_inverted_with_length(): # Boundary between upstream and transcript. invariant( crossmap.coordinate_to_noncoding, - Coord(72), + 72, crossmap.noncoding_to_coordinate, NonCodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_noncoding, - Coord(71), + 71, crossmap.noncoding_to_coordinate, NonCodingPoint(position=1, offset=0, region=''), ) @@ -313,13 +313,13 @@ def test_NonCoding_inverted_with_length(): # Boundary between downstream and transcript. invariant( crossmap.coordinate_to_noncoding, - Coord(5), + 5, crossmap.noncoding_to_coordinate, NonCodingPoint(position=22, offset=0, region=''), ) invariant( crossmap.coordinate_to_noncoding, - Coord(4), + 4, crossmap.noncoding_to_coordinate, NonCodingPoint(position=22, offset=1, region='d'), ) @@ -447,13 +447,13 @@ def test_NonCoding_location_end_equal_reference_length(): invariant( crossmap.coordinate_to_noncoding, - Coord(9), + 9, crossmap.noncoding_to_coordinate, NonCodingPoint(position=10, offset=0, region=''), ) with pytest.raises(ValueError) as error: - crossmap.coordinate_to_noncoding(Coord(10)) + crossmap.coordinate_to_noncoding(10) assert str(error.value) == 'Coordinate 10 is not within the bounds of the reference length 10.' @@ -538,13 +538,13 @@ def test_Coding(): # Boundary between upstream and 5' UTR. invariant( crossmap.coordinate_to_coding, - Coord(4), + 4, crossmap.coding_to_coordinate, CodingPoint(position=11, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_coding, - Coord(5), + 5, crossmap.coding_to_coordinate, CodingPoint(position=11, offset=0, region='-'), ) @@ -552,13 +552,13 @@ def test_Coding(): # Boundary between 5' and CDS. invariant( crossmap.coordinate_to_coding, - Coord(31), + 31, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='-'), ) invariant( crossmap.coordinate_to_coding, - Coord(32), + 32, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region=''), ) @@ -566,13 +566,13 @@ def test_Coding(): # Boundary between CDS and 3'. invariant( crossmap.coordinate_to_coding, - Coord(42), + 42, crossmap.coding_to_coordinate, CodingPoint(position=6, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, - Coord(43), + 43, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='*'), ) @@ -580,13 +580,13 @@ def test_Coding(): # Boundary between 3' and downstream. invariant( crossmap.coordinate_to_coding, - Coord(71), + 71, crossmap.coding_to_coordinate, CodingPoint(position=5, offset=0, region='*'), ) invariant( crossmap.coordinate_to_coding, - Coord(72), + 72, crossmap.coding_to_coordinate, CodingPoint(position=5, offset=1, region='d'), ) @@ -598,13 +598,13 @@ def test_Coding_with_length(): # Boundary between upstream and 5' UTR. invariant( crossmap.coordinate_to_coding, - Coord(4), + 4, crossmap.coding_to_coordinate, CodingPoint(position=11, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_coding, - Coord(5), + 5, crossmap.coding_to_coordinate, CodingPoint(position=11, offset=0, region='-'), ) @@ -612,13 +612,13 @@ def test_Coding_with_length(): # Boundary between 5' and CDS. invariant( crossmap.coordinate_to_coding, - Coord(31), + 31, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='-'), ) invariant( crossmap.coordinate_to_coding, - Coord(32), + 32, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region=''), ) @@ -626,13 +626,13 @@ def test_Coding_with_length(): # Boundary between CDS and 3'. invariant( crossmap.coordinate_to_coding, - Coord(42), + 42, crossmap.coding_to_coordinate, CodingPoint(position=6, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, - Coord(43), + 43, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='*'), ) @@ -640,13 +640,13 @@ def test_Coding_with_length(): # Boundary between 3' and downstream. invariant( crossmap.coordinate_to_coding, - Coord(71), + 71, crossmap.coding_to_coordinate, CodingPoint(position=5, offset=0, region='*'), ) invariant( crossmap.coordinate_to_coding, - Coord(72), + 72, crossmap.coding_to_coordinate, CodingPoint(position=5, offset=1, region='d'), ) @@ -654,12 +654,12 @@ def test_Coding_with_length(): # Boundary between downstream and sequence end. invariant( crossmap.coordinate_to_coding, - Coord(74), + 74, crossmap.coding_to_coordinate, CodingPoint(position=5, offset=3, region='d'), ) with pytest.raises(ValueError) as error: - crossmap.coordinate_to_coding(Coord(75)) + crossmap.coordinate_to_coding(75) assert str(error.value) == 'Coordinate 75 is not within the bounds of the reference length 75.' with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=5, offset=4, region='d')) @@ -672,13 +672,13 @@ def test_Coding_inverted(): # Boundary between upstream and 5' UTR. invariant( crossmap.coordinate_to_coding, - Coord(72), + 72, crossmap.coding_to_coordinate, CodingPoint(position=5, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_coding, - Coord(71), + 71, crossmap.coding_to_coordinate, CodingPoint(position=5, offset=0, region='-'), ) @@ -686,13 +686,13 @@ def test_Coding_inverted(): # Boundary between 5' and CDS. invariant( crossmap.coordinate_to_coding, - Coord(43), + 43, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='-'), ) invariant( crossmap.coordinate_to_coding, - Coord(42), + 42, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region=''), ) @@ -700,13 +700,13 @@ def test_Coding_inverted(): # Boundary between CDS and 3'. invariant( crossmap.coordinate_to_coding, - Coord(32), + 32, crossmap.coding_to_coordinate, CodingPoint(position=6, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, - Coord(31), + 31, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='*'), ) @@ -714,13 +714,13 @@ def test_Coding_inverted(): # Boundary between 3' and downstream. invariant( crossmap.coordinate_to_coding, - Coord(5), + 5, crossmap.coding_to_coordinate, CodingPoint(position=11, offset=0, region='*'), ) invariant( crossmap.coordinate_to_coding, - Coord(4), + 4, crossmap.coding_to_coordinate, CodingPoint(position=11, offset=1, region='d'), ) @@ -732,14 +732,14 @@ def test_Coding_inverted_with_length(): # Boundary between upstream and sequence end. with pytest.raises(ValueError) as error: - crossmap.coordinate_to_coding(Coord(75)) + crossmap.coordinate_to_coding(75) assert str(error.value) == 'Coordinate 75 is not within the bounds of the reference length 75.' with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=5, offset=-4, region='u')) assert str(error.value) == 'Offset -4 exceeds upstream region.' invariant( crossmap.coordinate_to_coding, - Coord(74), + 74, crossmap.coding_to_coordinate, CodingPoint(position=5, offset=-3, region='u'), ) @@ -747,13 +747,13 @@ def test_Coding_inverted_with_length(): # Boundary between upstream and 5' UTR. invariant( crossmap.coordinate_to_coding, - Coord(72), + 72, crossmap.coding_to_coordinate, CodingPoint(position=5, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_coding, - Coord(71), + 71, crossmap.coding_to_coordinate, CodingPoint(position=5, offset=0, region='-'), ) @@ -761,13 +761,13 @@ def test_Coding_inverted_with_length(): # Boundary between 5' and CDS. invariant( crossmap.coordinate_to_coding, - Coord(43), + 43, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='-'), ) invariant( crossmap.coordinate_to_coding, - Coord(42), + 42, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region=''), ) @@ -775,13 +775,13 @@ def test_Coding_inverted_with_length(): # Boundary between CDS and 3'. invariant( crossmap.coordinate_to_coding, - Coord(32), + 32, crossmap.coding_to_coordinate, CodingPoint(position=6, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, - Coord(31), + 31, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='*'), ) @@ -789,13 +789,13 @@ def test_Coding_inverted_with_length(): # Boundary between 3' and downstream. invariant( crossmap.coordinate_to_coding, - Coord(5), + 5, crossmap.coding_to_coordinate, CodingPoint(position=11, offset=0, region='*'), ) invariant( crossmap.coordinate_to_coding, - Coord(4), + 4, crossmap.coding_to_coordinate, CodingPoint(position=11, offset=1, region='d'), ) @@ -808,13 +808,13 @@ def test_Coding_regions(): # Upstream odd length intron between two regions. invariant( crossmap.coordinate_to_coding, - Coord(25), + 25, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=5, region='-'), ) invariant( crossmap.coordinate_to_coding, - Coord(26), + 26, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=-4, region=''), ) @@ -822,13 +822,13 @@ def test_Coding_regions(): # Downstream odd length intron between two regions. invariant( crossmap.coordinate_to_coding, - Coord(44), + 44, crossmap.coding_to_coordinate, CodingPoint(position=10, offset=5, region=''), ) invariant( crossmap.coordinate_to_coding, - Coord(45), + 45, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=-4, region='*'), ) @@ -841,13 +841,13 @@ def test_Coding_regions_inverted(): # Upstream odd length intron between two regions. invariant( crossmap.coordinate_to_coding, - Coord(44), + 44, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=5, region='-'), ) invariant( crossmap.coordinate_to_coding, - Coord(43), + 43, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=-4, region=''), ) @@ -855,13 +855,13 @@ def test_Coding_regions_inverted(): # Downstream odd length intron between two regions. invariant( crossmap.coordinate_to_coding, - Coord(25), + 25, crossmap.coding_to_coordinate, CodingPoint(position=10, offset=5, region=''), ) invariant( crossmap.coordinate_to_coding, - Coord(24), + 24, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=-4, region='*'), ) @@ -874,13 +874,13 @@ def test_Coding_no_utr5(): # Direct transition from upstream to CDS. invariant( crossmap.coordinate_to_coding, - Coord(9), + 9, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_coding, - Coord(10), + 10, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region=''), ) @@ -893,13 +893,13 @@ def test_Coding_no_utr5_inverted(): # Direct transition from upstream to CDS. invariant( crossmap.coordinate_to_coding, - Coord(20), + 20, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_coding, - Coord(19), + 19, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region=''), ) @@ -911,13 +911,13 @@ def test_Coding_no_utr3(): # Direct transition from CDS to downstream. invariant( crossmap.coordinate_to_coding, - Coord(19), + 19, crossmap.coding_to_coordinate, CodingPoint(position=5, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, - Coord(20), + 20, crossmap.coding_to_coordinate, CodingPoint(position=5, offset=1, region='d'), ) @@ -929,13 +929,13 @@ def test_Coding_no_utr3_unequal_utr5(): # Direct transition from CDS to downstream. invariant( crossmap.coordinate_to_coding, - Coord(19), + 19, crossmap.coding_to_coordinate, CodingPoint(position=8, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, - Coord(20), + 20, crossmap.coding_to_coordinate, CodingPoint(position=8, offset=1, region='d'), ) @@ -948,13 +948,13 @@ def test_Coding_no_utr3_inverted(): # Direct transition from CDS to downstream. invariant( crossmap.coordinate_to_coding, - Coord(10), + 10, crossmap.coding_to_coordinate, CodingPoint(position=5, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, - Coord(9), + 9, crossmap.coding_to_coordinate, CodingPoint(position=5, offset=1, region='d'), ) @@ -967,13 +967,13 @@ def test_Coding_no_utr3_inverted_unequal_utr5(): # Direct transition from CDS to downstream. invariant( crossmap.coordinate_to_coding, - Coord(10), + 10, crossmap.coding_to_coordinate, CodingPoint(position=8, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, - Coord(9), + 9, crossmap.coding_to_coordinate, CodingPoint(position=8, offset=1, region='d'), ) @@ -986,19 +986,19 @@ def test_Coding_small_utr5(): # Transition from upstream to 5' UTR to CDS. invariant( crossmap.coordinate_to_coding, - Coord(9), + 9, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_coding, - Coord(10), + 10, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='-'), ) invariant( crossmap.coordinate_to_coding, - Coord(11), + 11, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region=''), ) @@ -1011,19 +1011,19 @@ def test_Coding_small_utr5_inverted(): # Transition from upstream to 5' UTR to CDS. invariant( crossmap.coordinate_to_coding, - Coord(20), + 20, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=-1, region='u'), ) invariant( crossmap.coordinate_to_coding, - Coord(19), + 19, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='-'), ) invariant( crossmap.coordinate_to_coding, - Coord(18), + 18, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region=''), ) @@ -1036,19 +1036,19 @@ def test_Coding_small_utr3(): # Transition from CDS to 3' UTR to downstream. invariant( crossmap.coordinate_to_coding, - Coord(18), + 18, crossmap.coding_to_coordinate, CodingPoint(position=4, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, - Coord(19), + 19, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='*'), ) invariant( crossmap.coordinate_to_coding, - Coord(20), + 20, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=1, region='d'), ) @@ -1061,19 +1061,19 @@ def test_Coding_small_utr3_inverted(): # Transition from CDS to 3' UTR to downstream. invariant( crossmap.coordinate_to_coding, - Coord(11), + 11, crossmap.coding_to_coordinate, CodingPoint(position=4, offset=0, region=''), ) invariant( crossmap.coordinate_to_coding, - Coord(10), + 10, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region='*'), ) invariant( crossmap.coordinate_to_coding, - Coord(9), + 9, crossmap.coding_to_coordinate, CodingPoint(position=1, offset=1, region='d'), ) @@ -1084,7 +1084,7 @@ def test_Coding_no_intron(): invariant( crossmap.coordinate_to_coding, - Coord(20), + 20, crossmap.coding_to_coordinate, CodingPoint(position=6, offset=0, region=''), ) @@ -1095,7 +1095,7 @@ def test_Coding_no_intron_inverted(): invariant( crossmap.coordinate_to_coding, - Coord(20), + 20, crossmap.coding_to_coordinate, CodingPoint(position=5, offset=0, region=''), ) @@ -1106,7 +1106,7 @@ def test_Coding_one_base_intron(): invariant( crossmap.coordinate_to_coding, - Coord(19), + 19, crossmap.coding_to_coordinate, CodingPoint(position=4, offset=1, region=''), ) @@ -1117,7 +1117,7 @@ def test_Coding_one_base_intron_inverted(): invariant( crossmap.coordinate_to_coding, - Coord(19), + 19, crossmap.coding_to_coordinate, CodingPoint(position=5, offset=1, region=''), ) @@ -1130,7 +1130,7 @@ def test_Coding_degenerate(): # Degenerate position in upstream. degenerate_equal( crossmap.coding_to_coordinate, - Coord(9), + 9, [ CodingPoint(position=1, offset=-1, region='u'), CodingPoint(position=2, offset=0, region='-'), @@ -1138,7 +1138,7 @@ def test_Coding_degenerate(): ) degenerate_equal( crossmap.coding_to_coordinate, - Coord(20), + 20, [ CodingPoint(position=1, offset=1, region='d'), CodingPoint(position=2, offset=0, region='*'), @@ -1152,7 +1152,7 @@ def test_Coding_inverted_degenerate(): degenerate_equal( crossmap.coding_to_coordinate, - Coord(20), + 20, [ CodingPoint(position=1, offset=-1, region='u'), CodingPoint(position=2, offset=0, region='-'), @@ -1160,7 +1160,7 @@ def test_Coding_inverted_degenerate(): ) degenerate_equal( crossmap.coding_to_coordinate, - Coord(9), + 9, [ CodingPoint(position=1, offset=1, region='d'), CodingPoint(position=2, offset=0, region='*'), @@ -1172,47 +1172,47 @@ def test_Coding_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" crossmap = Coding([(10, 20)], (11, 19)) - assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=2, offset=0, region='-') - assert crossmap.coordinate_to_coding(Coord(20), True) == CodingPoint(position=2, offset=0, region='*') + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=2, offset=0, region='-') + assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=2, offset=0, region='*') def test_Coding_inverted_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" crossmap = Coding([(10, 20)], (11, 19), inverted=True) - assert crossmap.coordinate_to_coding(Coord(20), True) == CodingPoint(position=2, offset=0, region='-') - assert crossmap.coordinate_to_coding(Coord(25), True) == CodingPoint(position=7, offset=0, region='-') - assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=2, offset=0, region='*') + assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=2, offset=0, region='-') + assert crossmap.coordinate_to_coding(25, True) == CodingPoint(position=7, offset=0, region='-') + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=2, offset=0, region='*') def test_Coding_no_utr5_degenerate_return(): """A 5' UTR may be missing.""" crossmap = Coding([(10, 20)], (10, 15)) - assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=1, offset=0, region='-') - assert crossmap.coordinate_to_coding(Coord(10), True) == CodingPoint(position=1, offset=0, region='') - assert crossmap.coordinate_to_coding(Coord(19), True) == CodingPoint(position=5, offset=0, region='*') - assert crossmap.coordinate_to_coding(Coord(20), True) == CodingPoint(position=6, offset=0, region='*') + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=1, offset=0, region='-') + assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=1, offset=0, region='') + assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=5, offset=0, region='*') + assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=6, offset=0, region='*') def test_Coding_no_utr5_inverted_degenerate_return(): """A 5' UTR may be missing.""" crossmap = Coding([(10, 20)], (10, 15), inverted=True) - assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=1, offset=0, region='*') - assert crossmap.coordinate_to_coding(Coord(10), True) == CodingPoint(position=5, offset=0, region='') - assert crossmap.coordinate_to_coding(Coord(19), True) == CodingPoint(position=5, offset=0, region='-') - assert crossmap.coordinate_to_coding(Coord(20), True) == CodingPoint(position=6, offset=0, region='-') + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=1, offset=0, region='*') + assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=5, offset=0, region='') + assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=5, offset=0, region='-') + assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=6, offset=0, region='-') def test_Coding_no_utr3_degenerate_return(): """A 3' UTR may be missing.""" crossmap = Coding([(10, 20)], (15, 20)) - assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=6, offset=0, region='-') - assert crossmap.coordinate_to_coding(Coord(10), True) == CodingPoint(position=5, offset=0, region='-') - assert crossmap.coordinate_to_coding(Coord(19), True) == CodingPoint(position=5, offset=0, region='') - assert crossmap.coordinate_to_coding(Coord(20), True) == CodingPoint(position=1, offset=0, region='*') + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=6, offset=0, region='-') + assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=5, offset=0, region='-') + assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=5, offset=0, region='') + assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=1, offset=0, region='*') def test_Coding_no_utr3_inverted_degenerate_return(): @@ -1220,70 +1220,70 @@ def test_Coding_no_utr3_inverted_degenerate_return(): crossmap = Coding([(10, 20)], (15, 20), inverted=True) - assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=6, offset=0, region='*') - assert crossmap.coordinate_to_coding(Coord(10), True) == CodingPoint(position=5, offset=0, region='*') - assert crossmap.coordinate_to_coding(Coord(19), True) == CodingPoint(position=1, offset=0, region='') - assert crossmap.coordinate_to_coding(Coord(20), True) == CodingPoint(position=1, offset=0, region='-') + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=6, offset=0, region='*') + assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=5, offset=0, region='*') + assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=1, offset=0, region='') + assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=1, offset=0, region='-') def test_Coding_small_utr5_degenerate_return(): """A 5' UTR may be of length one.""" crossmap = Coding([(10, 20)], (11, 15)) - assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=2, offset=0, region='-') - assert crossmap.coordinate_to_coding(Coord(10), True) == CodingPoint(position=1, offset=0, region='-') - assert crossmap.coordinate_to_coding(Coord(11), True) == CodingPoint(position=1, offset=0, region='') + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=2, offset=0, region='-') + assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=1, offset=0, region='-') + assert crossmap.coordinate_to_coding(11, True) == CodingPoint(position=1, offset=0, region='') def test_Coding_small_utr5_inverted_degenerate_return(): """A 5' UTR may be of length one.""" crossmap = Coding([(10, 20)], (11, 15), inverted=True) - assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=2, offset=0, region='*') - assert crossmap.coordinate_to_coding(Coord(10), True) == CodingPoint(position=1, offset=0, region='*') - assert crossmap.coordinate_to_coding(Coord(11), True) == CodingPoint(position=4, offset=0, region='') + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=2, offset=0, region='*') + assert crossmap.coordinate_to_coding(10, True) == CodingPoint(position=1, offset=0, region='*') + assert crossmap.coordinate_to_coding(11, True) == CodingPoint(position=4, offset=0, region='') def test_Coding_small_utr3_degenerate_return(): """A 3' UTR may be of length one.""" crossmap = Coding([(10, 20)], (15, 19), inverted=False) - assert crossmap.coordinate_to_coding(Coord(18), True) == CodingPoint(position=4, offset=0, region='') - assert crossmap.coordinate_to_coding(Coord(19), True) == CodingPoint(position=1, offset=0, region='*') - assert crossmap.coordinate_to_coding(Coord(20), True) == CodingPoint(position=2, offset=0, region='*') + assert crossmap.coordinate_to_coding(18, True) == CodingPoint(position=4, offset=0, region='') + assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=1, offset=0, region='*') + assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=2, offset=0, region='*') def test_Coding_small_utr3_inverted_degenerate_return(): """A 3' UTR may be of length one.""" crossmap = Coding([(10, 20)], (15, 19), inverted=True) - assert crossmap.coordinate_to_coding(Coord(18), True) == CodingPoint(position=1, offset=0, region='') - assert crossmap.coordinate_to_coding(Coord(19), True) == CodingPoint(position=1, offset=0, region='-') - assert crossmap.coordinate_to_coding(Coord(20), True) == CodingPoint(position=2, offset=0, region='-') + assert crossmap.coordinate_to_coding(18, True) == CodingPoint(position=1, offset=0, region='') + assert crossmap.coordinate_to_coding(19, True) == CodingPoint(position=1, offset=0, region='-') + assert crossmap.coordinate_to_coding(20, True) == CodingPoint(position=2, offset=0, region='-') def test_Coding_two_exons_inverted_degenerate_return(): """Degenerate upstream and downstream positions may be returned.""" crossmap = Coding([(10, 20), (30, 40)], (18, 37), inverted=True) - assert crossmap.coordinate_to_coding(Coord(5), True) == CodingPoint(position=13, offset=0, region='*') - assert crossmap.coordinate_to_coding(Coord(25), True) == CodingPoint(position=7, offset=5, region='') - assert crossmap.coordinate_to_coding(Coord(35), True) == CodingPoint(position=2, offset=0, region='') - assert crossmap.coordinate_to_coding(Coord(38), True) == CodingPoint(position=2, offset=0, region='-') + assert crossmap.coordinate_to_coding(5, True) == CodingPoint(position=13, offset=0, region='*') + assert crossmap.coordinate_to_coding(25, True) == CodingPoint(position=7, offset=5, region='') + assert crossmap.coordinate_to_coding(35, True) == CodingPoint(position=2, offset=0, region='') + assert crossmap.coordinate_to_coding(38, True) == CodingPoint(position=2, offset=0, region='-') def test_Coding_degenerate_no_return(): """Degenerate internal positions do not exist.""" crossmap = Coding([(10, 20), (30, 40)], (10, 40)) - assert crossmap.coordinate_to_coding(Coord(25)) == crossmap.coordinate_to_coding(Coord(25), True) + assert crossmap.coordinate_to_coding(25) == crossmap.coordinate_to_coding(25, True) def test_Coding_inverted_degenerate_no_return(): """Degenerate internal positions do not exist.""" crossmap = Coding([(10, 20), (30, 40)], (10, 40), inverted=True) - assert crossmap.coordinate_to_coding(Coord(25)) == crossmap.coordinate_to_coding(Coord(25), True) + assert crossmap.coordinate_to_coding(25) == crossmap.coordinate_to_coding(25, True) def test_Coding_no_utr_degenerate(): @@ -1292,7 +1292,7 @@ def test_Coding_no_utr_degenerate(): degenerate_equal( crossmap.coding_to_coordinate, - Coord(9), + 9, [ CodingPoint(position=1, offset=-1, region='u'), CodingPoint(position=1, offset=0, region='-'), @@ -1300,7 +1300,7 @@ def test_Coding_no_utr_degenerate(): ) degenerate_equal( crossmap.coding_to_coordinate, - Coord(11), + 11, [ CodingPoint(position=1, offset=1, region='d'), CodingPoint(position=1, offset=0, region='*'), @@ -1314,7 +1314,7 @@ def test_Coding_inverted_no_utr_degenerate(): degenerate_equal( crossmap.coding_to_coordinate, - Coord(11), + 11, [ CodingPoint(position=1, offset=-1, region='u'), CodingPoint(position=1, offset=0, region='-'), @@ -1322,7 +1322,7 @@ def test_Coding_inverted_no_utr_degenerate(): ) degenerate_equal( crossmap.coding_to_coordinate, - Coord(9), + 9, [ CodingPoint(position=1, offset=1, region='d'), CodingPoint(position=1, offset=0, region='*'), @@ -1334,18 +1334,18 @@ def test_Coding_no_utr_degenerate_return(): """UTRs may be missing.""" crossmap = Coding([(10, 11)], (10, 11)) - assert crossmap.coordinate_to_coding(Coord(8), True) == CodingPoint(position=2, offset=0, region='-') - assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=1, offset=0, region='-') - assert crossmap.coordinate_to_coding(Coord(11), True) == CodingPoint(position=1, offset=0, region='*') - assert crossmap.coordinate_to_coding(Coord(12), True) == CodingPoint(position=2, offset=0, region='*') + assert crossmap.coordinate_to_coding(8, True) == CodingPoint(position=2, offset=0, region='-') + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=1, offset=0, region='-') + assert crossmap.coordinate_to_coding(11, True) == CodingPoint(position=1, offset=0, region='*') + assert crossmap.coordinate_to_coding(12, True) == CodingPoint(position=2, offset=0, region='*') def test_Coding_inverted_no_utr_degenerate_return(): """UTRs may be missing.""" crossmap = Coding([(10, 11)], (10, 11), inverted=True) - assert crossmap.coordinate_to_coding(Coord(11), True) == CodingPoint(position=1, offset=0, region='-') - assert crossmap.coordinate_to_coding(Coord(9), True) == CodingPoint(position=1, offset=0, region='*') + assert crossmap.coordinate_to_coding(11, True) == CodingPoint(position=1, offset=0, region='-') + assert crossmap.coordinate_to_coding(9, True) == CodingPoint(position=1, offset=0, region='*') def test_Coding_invalid_position(): @@ -1500,13 +1500,13 @@ def test_Coding_protein(): # Boundary between upstream and 5' UTR invariant( crossmap.coordinate_to_protein, - Coord(4), + 4, crossmap.protein_to_coordinate, ProteinPoint(position=4, offset=-1, region='u', position_in_codon=2) ) invariant( crossmap.coordinate_to_protein, - Coord(5), + 5, crossmap.protein_to_coordinate, ProteinPoint(position=4, offset=0, region='-', position_in_codon=2) ) @@ -1514,13 +1514,13 @@ def test_Coding_protein(): # Boundary between 5' UTR and CDS invariant( crossmap.coordinate_to_protein, - Coord(31), + 31, crossmap.protein_to_coordinate, ProteinPoint(position=1, offset=0, region='-', position_in_codon=3), ) invariant( crossmap.coordinate_to_protein, - Coord(32), + 32, crossmap.protein_to_coordinate, ProteinPoint(position=1, offset=0, region='', position_in_codon=1), ) @@ -1528,13 +1528,13 @@ def test_Coding_protein(): # Intron boundary. invariant( crossmap.coordinate_to_protein, - Coord(34), + 34, crossmap.protein_to_coordinate, ProteinPoint(position=1, offset=0, region='', position_in_codon=3), ) invariant( crossmap.coordinate_to_protein, - Coord(35), + 35, crossmap.protein_to_coordinate, ProteinPoint(position=1, offset=1, region='', position_in_codon=3), ) @@ -1542,13 +1542,13 @@ def test_Coding_protein(): # Boundary between CDS and 3' UTR. invariant( crossmap.coordinate_to_protein, - Coord(42), + 42, crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=0, region='', position_in_codon=3), ) invariant( crossmap.coordinate_to_protein, - Coord(43), + 43, crossmap.protein_to_coordinate, ProteinPoint(position=1, offset=0, region='*', position_in_codon=1), ) @@ -1556,13 +1556,13 @@ def test_Coding_protein(): # Boundary between 3' UTR and downstream invariant( crossmap.coordinate_to_protein, - Coord(71), + 71, crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=0, region='*', position_in_codon=2) ) invariant( crossmap.coordinate_to_protein, - Coord(72), + 72, crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=1, region='d', position_in_codon=2) ) @@ -1575,13 +1575,13 @@ def test_Coding_inverted_protein(): # Boundary between upstream and 5' UTR invariant( crossmap.coordinate_to_protein, - Coord(4), + 4, crossmap.protein_to_coordinate, ProteinPoint(position=4, offset=1, region='d', position_in_codon=2) ) invariant( crossmap.coordinate_to_protein, - Coord(5), + 5, crossmap.protein_to_coordinate, ProteinPoint(position=4, offset=0, region='*', position_in_codon=2) ) @@ -1589,13 +1589,13 @@ def test_Coding_inverted_protein(): # Boundary between 5' UTR and CDS invariant( crossmap.coordinate_to_protein, - Coord(31), + 31, crossmap.protein_to_coordinate, ProteinPoint(position=1, offset=0, region='*', position_in_codon=1), ) invariant( crossmap.coordinate_to_protein, - Coord(32), + 32, crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=0, region='', position_in_codon=3), ) @@ -1603,13 +1603,13 @@ def test_Coding_inverted_protein(): # Intron boundary. invariant( crossmap.coordinate_to_protein, - Coord(34), + 34, crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=0, region='', position_in_codon=1), ) invariant( crossmap.coordinate_to_protein, - Coord(35), + 35, crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=-1, region='', position_in_codon=1), ) @@ -1617,13 +1617,13 @@ def test_Coding_inverted_protein(): # Boundary between CDS and 3' UTR. invariant( crossmap.coordinate_to_protein, - Coord(42), + 42, crossmap.protein_to_coordinate, ProteinPoint(position=1, offset=0, region='', position_in_codon=1), ) invariant( crossmap.coordinate_to_protein, - Coord(43), + 43, crossmap.protein_to_coordinate, ProteinPoint(position=1, offset=0, region='-', position_in_codon=3), ) @@ -1631,13 +1631,13 @@ def test_Coding_inverted_protein(): # Boundary between 3' UTR and downstream invariant( crossmap.coordinate_to_protein, - Coord(71), + 71, crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=0, region='-', position_in_codon=2) ) invariant( crossmap.coordinate_to_protein, - Coord(72), + 72, crossmap.protein_to_coordinate, ProteinPoint(position=2, offset=-1, region='u', position_in_codon=2) ) @@ -1650,7 +1650,7 @@ def test_Coding_protein_degenerate(): # Degenerate position in upstream. degenerate_equal( crossmap.protein_to_coordinate, - Coord(9), + 9, [ ProteinPoint(position=1, offset=-1, region='u', position_in_codon=2), ProteinPoint(position=1, offset=0, region='-', position_in_codon=1), @@ -1659,7 +1659,7 @@ def test_Coding_protein_degenerate(): # Degenerate position in downstream. degenerate_equal( crossmap.protein_to_coordinate, - Coord(20), + 20, [ ProteinPoint(position=1, offset=1, region='d', position_in_codon=2), ProteinPoint(position=1, offset=0, region='*', position_in_codon=3), @@ -1674,7 +1674,7 @@ def test_Coding_inverted_protein_degenerate(): # Degenerate position in upstream. degenerate_equal( crossmap.protein_to_coordinate, - Coord(20), + 20, [ ProteinPoint(position=1, offset=-1, region='u', position_in_codon=2), ProteinPoint(position=1, offset=0, region='-', position_in_codon=1), @@ -1683,7 +1683,7 @@ def test_Coding_inverted_protein_degenerate(): # Degenerate position in downstream. degenerate_equal( crossmap.protein_to_coordinate, - Coord(9), + 9, [ ProteinPoint(position=1, offset=1, region='d', position_in_codon=2), ProteinPoint(position=1, offset=0, region='*', position_in_codon=3), @@ -1697,11 +1697,11 @@ def test_Coding_cds_end_equal_reference_length(): invariant( crossmap.coordinate_to_coding, - Coord(9), + 9, crossmap.coding_to_coordinate, CodingPoint(position=10, offset=0, region=''), ) with pytest.raises(ValueError) as error: - crossmap.coordinate_to_coding(Coord(10)) + crossmap.coordinate_to_coding(10) assert str(error.value) == 'Coordinate 10 is not within the bounds of the reference length 10.' diff --git a/tests/test_locus.py b/tests/test_locus.py index 93c4609..451be6d 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -1,7 +1,7 @@ import pytest from helper import invariant -from mutalyzer_crossmapper.locus import Coord, Locus, Point +from mutalyzer_crossmapper.locus import Locus, Point def test_invalid_locus_initialization(): @@ -55,25 +55,27 @@ def test_invalid_locus_initialization(): assert str(error.value) == 'Locus start 10 must be smaller than locus end 10.' -def test_invalid_coord_initialization(): - """Test Coord initialization.""" +def test_invalid_locus_coordinate(): + """Forward orientent Locus with invalid coordinate.""" + locus = Locus((30, 35)) + with pytest.raises(ValueError) as error: - Coord(-1) + locus.to_position(-1) assert str(error.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as error: - Coord(3.5) + locus.to_position(3.5) assert str(error.value) == 'Value must be an integer.' with pytest.raises(ValueError) as error: - Coord('10') + locus.to_position('10') assert str(error.value) == 'Value must be an integer.' with pytest.raises(ValueError) as error: - Coord(None) + locus.to_position(None) assert str(error.value) == 'Value must be an integer.' with pytest.raises(ValueError) as error: - Coord([10]) + locus.to_position([10]) assert str(error.value) == 'Value must be an integer.' with pytest.raises(ValueError) as error: - Coord(True) + locus.to_position(True) assert str(error.value) == 'Value must be an integer.' @@ -124,21 +126,21 @@ def test_locus(): """Forward orientent Locus.""" locus = Locus((30, 35)) - invariant(locus.to_position, Coord(29), locus.to_coordinate, Point(position=0, offset=-1)) - invariant(locus.to_position, Coord(30), locus.to_coordinate, Point(position=0, offset=0)) - invariant(locus.to_position, Coord(31), locus.to_coordinate, Point(position=1, offset=0)) - invariant(locus.to_position, Coord(33), locus.to_coordinate, Point(position=3, offset=0)) - invariant(locus.to_position, Coord(34), locus.to_coordinate, Point(position=4, offset=0)) - invariant(locus.to_position, Coord(35), locus.to_coordinate, Point(position=4, offset=1)) + invariant(locus.to_position, 29, locus.to_coordinate, Point(position=0, offset=-1)) + invariant(locus.to_position, 30, locus.to_coordinate, Point(position=0, offset=0)) + invariant(locus.to_position, 31, locus.to_coordinate, Point(position=1, offset=0)) + invariant(locus.to_position, 33, locus.to_coordinate, Point(position=3, offset=0)) + invariant(locus.to_position, 34, locus.to_coordinate, Point(position=4, offset=0)) + invariant(locus.to_position, 35, locus.to_coordinate, Point(position=4, offset=1)) def test_locus_inverted(): """Reverse orientent Locus.""" locus = Locus((30, 35), True) - invariant(locus.to_position, Coord(35), locus.to_coordinate, Point(position=0, offset=-1)) - invariant(locus.to_position, Coord(34), locus.to_coordinate, Point(position=0, offset=0)) - invariant(locus.to_position, Coord(33), locus.to_coordinate, Point(position=1, offset=0)) - invariant(locus.to_position, Coord(31), locus.to_coordinate, Point(position=3, offset=0)) - invariant(locus.to_position, Coord(30), locus.to_coordinate, Point(position=4, offset=0)) - invariant(locus.to_position, Coord(29), locus.to_coordinate, Point(position=4, offset=1)) + invariant(locus.to_position, 35, locus.to_coordinate, Point(position=0, offset=-1)) + invariant(locus.to_position, 34, locus.to_coordinate, Point(position=0, offset=0)) + invariant(locus.to_position, 33, locus.to_coordinate, Point(position=1, offset=0)) + invariant(locus.to_position, 31, locus.to_coordinate, Point(position=3, offset=0)) + invariant(locus.to_position, 30, locus.to_coordinate, Point(position=4, offset=0)) + invariant(locus.to_position, 29, locus.to_coordinate, Point(position=4, offset=1)) diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index 3e4abd7..a217938 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -1,5 +1,5 @@ from mutalyzer_crossmapper import multi_locus -from mutalyzer_crossmapper.multi_locus import _offsets, Coord, MultiLocus, Point +from mutalyzer_crossmapper.multi_locus import _offsets, MultiLocus, Point from helper import invariant import pytest @@ -103,13 +103,13 @@ def test_MultiLocus_invalid_coordinate(): """Forward orientent MultiLocus with invalid coordinate.""" multi_locus = MultiLocus([(30, 35), (40, 45)]) with pytest.raises(ValueError) as error: - multi_locus.to_position(Coord(-1)) + multi_locus.to_position(-1) assert str(error.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as error: - multi_locus.to_position(Coord(46.7)) + multi_locus.to_position(46.7) assert str(error.value) == 'Value must be an integer.' with pytest.raises(ValueError) as error: - multi_locus.to_position(Coord('31')) + multi_locus.to_position('31') assert str(error.value) == 'Value must be an integer.' @@ -136,14 +136,14 @@ def test_MultiLocus(): # Boundary between upstream and the first locus. invariant( multi_locus.to_position, - Coord(4), + 4, multi_locus.to_coordinate, Point(position=0, offset=-1, region='u'), ) invariant( multi_locus.to_position, - Coord(5), + 5, multi_locus.to_coordinate, Point(position=0, offset=0, region=''), ) @@ -151,37 +151,37 @@ def test_MultiLocus(): # Internal locus. invariant( multi_locus.to_position, - Coord(29), + 29, multi_locus.to_coordinate, Point(position=9, offset=-1, region=''), ) invariant( multi_locus.to_position, - Coord(30), + 30, multi_locus.to_coordinate, Point(position=9, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(31), + 31, multi_locus.to_coordinate, Point(position=10, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(33), + 33, multi_locus.to_coordinate, Point(position=12, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(34), + 34, multi_locus.to_coordinate, Point(position=13, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(35), + 35, multi_locus.to_coordinate, Point(position=13, offset=1, region=''), ) @@ -189,13 +189,13 @@ def test_MultiLocus(): # Boundary between the last locus and downstream. invariant( multi_locus.to_position, - Coord(71), + 71, multi_locus.to_coordinate, Point(position=21, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(72), + 72, multi_locus.to_coordinate, Point(position=21, offset=1, region='d'), ) @@ -208,13 +208,13 @@ def test_MultiLocus_inverted(): # Boundary between upstream and the first locus. invariant( multi_locus.to_position, - Coord(72), + 72, multi_locus.to_coordinate, Point(position=0, offset=-1, region='u'), ) invariant( multi_locus.to_position, - Coord(71), + 71, multi_locus.to_coordinate, Point(position=0, offset=0, region=''), ) @@ -222,37 +222,37 @@ def test_MultiLocus_inverted(): # Internal locus. invariant( multi_locus.to_position, - Coord(35), + 35, multi_locus.to_coordinate, Point(position=8, offset=-1, region=''), ) invariant( multi_locus.to_position, - Coord(34), + 34, multi_locus.to_coordinate, Point(position=8, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(33), + 33, multi_locus.to_coordinate, Point(position=9, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(31), + 31, multi_locus.to_coordinate, Point(position=11, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(30), + 30, multi_locus.to_coordinate, Point(position=12, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(29), + 29, multi_locus.to_coordinate, Point(position=12, offset=1, region=''), ) @@ -260,13 +260,13 @@ def test_MultiLocus_inverted(): # Boundary between the last locus and downstream. invariant( multi_locus.to_position, - Coord(5), + 5, multi_locus.to_coordinate, Point(position=21, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(4), + 4, multi_locus.to_coordinate, Point(position=21, offset=1, region='d'), ) @@ -279,25 +279,25 @@ def test_MultiLocus_with_length(): # Boundary between the last locus and downstream. invariant( multi_locus.to_position, - Coord(71), + 71, multi_locus.to_coordinate, Point(position=21, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(72), + 72, multi_locus.to_coordinate, Point(position=21, offset=1, region='d'), ) invariant( multi_locus.to_position, - Coord(73), + 73, multi_locus.to_coordinate, Point(position=21, offset=2, region='d'), ) # Boundary between the last base and beyond the last base. with pytest.raises(ValueError) as error: - multi_locus.to_position(Coord(74)) + multi_locus.to_position(74) with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=21, offset=3, region='d')) @@ -309,25 +309,25 @@ def test_MultiLocus_inverted_with_length(): # Boundary between the first locus and upstream. invariant( multi_locus.to_position, - Coord(71), + 71, multi_locus.to_coordinate, Point(position=0, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(72), + 72, multi_locus.to_coordinate, Point(position=0, offset=-1, region='u'), ) # Boundary between the first base beyond the first base. invariant( multi_locus.to_position, - Coord(73), + 73, multi_locus.to_coordinate, Point(position=0, offset=-2, region='u'), ) with pytest.raises(ValueError) as error: - multi_locus.to_position(Coord(74)) + multi_locus.to_position(74) with pytest.raises(ValueError) as error: multi_locus.to_coordinate(Point(position=0, offset=-3, region='u')) @@ -338,13 +338,13 @@ def test_MultiLocus_adjacent_loci(): invariant( multi_locus.to_position, - Coord(2), + 2, multi_locus.to_coordinate, Point(position=1, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(3), + 3, multi_locus.to_coordinate, Point(position=2, offset=0, region=''), ) @@ -356,13 +356,13 @@ def test_MultiLocus_adjacent_loci_inverted(): invariant( multi_locus.to_position, - Coord(3), + 3, multi_locus.to_coordinate, Point(position=1, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(2), + 2, multi_locus.to_coordinate, Point(position=2, offset=0, region=''), ) @@ -374,13 +374,13 @@ def test_MultiLocus_offsets_odd(): invariant( multi_locus.to_position, - Coord(4), + 4, multi_locus.to_coordinate, Point(position=1, offset=2, region=''), ) invariant( multi_locus.to_position, - Coord(5), + 5, multi_locus.to_coordinate, Point(position=2, offset=-1, region=''), ) @@ -391,13 +391,13 @@ def test_MultiLocus_offsets_odd_inverted(): multi_locus = MultiLocus([(1, 3), (6, 8)], inverted=True) invariant( multi_locus.to_position, - Coord(4), + 4, multi_locus.to_coordinate, Point(position=1, offset=2, region=''), ) invariant( multi_locus.to_position, - Coord(3), + 3, multi_locus.to_coordinate, Point(position=2, offset=-1, region=''), ) @@ -409,13 +409,13 @@ def test_MultiLocus_offsets_even(): invariant( multi_locus.to_position, - Coord(4), + 4, multi_locus.to_coordinate, Point(position=1, offset=2, region=''), ) invariant( multi_locus.to_position, - Coord(5), + 5, multi_locus.to_coordinate, Point(position=2, offset=-2, region=''), ) @@ -427,13 +427,13 @@ def test_MultiLocus_offsets_even_inverted(): invariant( multi_locus.to_position, - Coord(5), + 5, multi_locus.to_coordinate, Point(position=1, offset=2, region=''), ) invariant( multi_locus.to_position, - Coord(4), + 4, multi_locus.to_coordinate, Point(position=2, offset=-2, region=''), ) @@ -444,37 +444,37 @@ def test_one_base_exon(): multi_locus = MultiLocus([(1, 2), (4, 5)]) invariant( multi_locus.to_position, - Coord(0), + 0, multi_locus.to_coordinate, Point(position=0, offset=-1, region='u'), ) invariant( multi_locus.to_position, - Coord(1), + 1, multi_locus.to_coordinate, Point(position=0, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(2), + 2, multi_locus.to_coordinate, Point(position=0, offset=1, region=''), ) invariant( multi_locus.to_position, - Coord(3), + 3, multi_locus.to_coordinate, Point(position=1, offset=-1, region=''), ) invariant( multi_locus.to_position, - Coord(4), + 4, multi_locus.to_coordinate, Point(position=1, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(5), + 5, multi_locus.to_coordinate, Point(position=1, offset=1, region='d'), ) @@ -485,37 +485,37 @@ def test_one_base_exon_inverted(): multi_locus = MultiLocus([(1, 2), (4, 5)], inverted=True) invariant( multi_locus.to_position, - Coord(0), + 0, multi_locus.to_coordinate, Point(position=1, offset=1, region='d'), ) invariant( multi_locus.to_position, - Coord(1), + 1, multi_locus.to_coordinate, Point(position=1, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(2), + 2, multi_locus.to_coordinate, Point(position=1, offset=-1, region=''), ) invariant( multi_locus.to_position, - Coord(3), + 3, multi_locus.to_coordinate, Point(position=0, offset=1, region=''), ) invariant( multi_locus.to_position, - Coord(4), + 4, multi_locus.to_coordinate, Point(position=0, offset=0, region=''), ) invariant( multi_locus.to_position, - Coord(5), + 5, multi_locus.to_coordinate, Point(position=0, offset=-1, region='u'), ) @@ -725,11 +725,11 @@ def test_MultiLocus_location_end_equal_reference_length(): invariant( multi_locus.to_position, - Coord(9), + 9, multi_locus.to_coordinate, Point(position=9, offset=0, region=''), ) with pytest.raises(ValueError) as error: - multi_locus.to_position(Coord(10)) + multi_locus.to_position(10) assert str(error.value) == 'Coordinate 10 is not within the bounds of the reference length 10.' From 0a1dbdd072ff21a5ddb8b8b1279f5d3fa4b116cf Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Sun, 6 Sep 2026 22:30:42 +0200 Subject: [PATCH 219/236] Reject negative coordinates and document the locus module. --- mutalyzer_crossmapper/locus.py | 120 ++++++++++++++++++++++++++------- tests/test_locus.py | 11 +++ 2 files changed, 108 insertions(+), 23 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index ce8b26e..76ae001 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -1,15 +1,11 @@ -from dataclasses import dataclass - - -@dataclass(slots=True) -class Point: - """Locus point dataclass.""" - position: int - offset: int = 0 +"""Conversions between coordinates and points for a single locus. - def __post_init__(self) -> None: - _check_non_negative_int(self.position) - _check_int(self.offset) +A locus is a half-open interval ``[start, end)`` defined on a sequence. +It can be addressed either by a coordinate, which is a zero-based index into +that sequence, or by a point, which is a zero-based position within the locus +plus an offset relative to its start or end. +""" +from dataclasses import dataclass def _check_int(value: int) -> None: @@ -32,26 +28,81 @@ def _check_positive_int(value: int) -> None: raise ValueError(f'Value {value} is not positive.') -def _check_locus(locus: tuple[int, int]) -> None: +@dataclass(slots=True) +class Point: + """A position within a locus, with an offset from its start or end. + + The offset is zero for positions inside the locus. A non-zero offset is + only valid at a boundary: negative at the start, positive at the end. + + :arg int position: Zero-based position within the locus. + :arg int offset: Offset relative to the locus start or end. + """ + position: int + offset: int = 0 + + def __post_init__(self) -> None: + _check_non_negative_int(self.position) + _check_int(self.offset) + + +def _check_locus(location: tuple[int, int]) -> None: """Check if the locus location is valid.""" - if not isinstance(locus, tuple) or len(locus) != 2: + if not isinstance(location, tuple) or len(location) != 2: raise ValueError('Locus must be a tuple of two values.') - for value in locus: + for value in location: _check_non_negative_int(value) - if locus[0] >= locus[1]: - raise ValueError(f'Locus start {locus[0]} must be smaller than locus end {locus[1]}.') + if location[0] >= location[1]: + raise ValueError( + f'Locus start {location[0]} must be smaller than locus end {location[1]}.') class Locus(): - """Locus object.""" + """Convert coordinates to and from points within a single locus. + + For ``Locus((10, 15))``, the half-open interval contains coordinates 10 to + 14 and positions 0 to 4. Coordinates outside the locus are represented + as boundary positions with offsets:: + + coordinate 8 9 10 11 12 13 14 15 16 + | | | | | | | | | + point position 0 0 0 1 2 3 4 4 4 + offset -2 -1 0 0 0 0 0 1 2 + + >>> from mutalyzer_crossmapper.locus import Locus, Point + >>> locus = Locus((10, 15)) + >>> locus.to_position(9) + Point(position=0, offset=-1) + >>> locus.to_position(15) + Point(position=4, offset=1) + >>> locus.to_coordinate(Point(position=4, offset=1)) + 15 + + For a locus on the reverse-complement strand, set ``inverted=True``:: + + coordinate 8 9 10 11 12 13 14 15 16 + | | | | | | | | | + point position 4 4 4 3 2 1 0 0 0 + offset 2 1 0 0 0 0 0 -1 -2 + + >>> inverted = Locus((10, 15), inverted=True) + >>> inverted.to_position(14) + Point(position=0, offset=0) + >>> inverted.to_position(15) + Point(position=0, offset=-1) + """ def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: """Initialize a Locus object. - :arg tuple location: Locus location. + :arg tuple location: Half-open interval, with the start strictly + smaller than the end. :arg bool inverted: Orientation. + + :raises ValueError: If the location is not a tuple of two non-negative + integers, or its start is not smaller than its end. """ _check_locus(location) @@ -60,7 +111,11 @@ def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: self._end = location[1] - location[0] # one-based length of the locus def _validate_point(self, position: int, offset: int) -> None: - """Validate a locus point dataclass according to HGVS rules.""" + """Check that a non-zero offset is at the matching locus boundary. + + Negative offsets belong at the start, positive ones at the end, and the + position must lie within the locus. + """ if offset != 0 and position not in (0, self._end - 1): raise ValueError(f'Position {position} is not at a locus boundary.') if offset < 0 and position != 0: @@ -73,9 +128,14 @@ def _validate_point(self, position: int, offset: int) -> None: def to_position(self, coordinate: int) -> Point: """Convert a coordinate to a locus point dataclass. - :arg int coordinate: Coordinate. + Coordinates outside the locus are converted to a boundary position + with a non-zero offset. + + :arg int coordinate: Zero-based coordinate, non-negative. :returns Point: Locus point dataclass. + + :raises ValueError: If the coordinate is not a non-negative integer. """ _check_non_negative_int(coordinate) @@ -95,12 +155,26 @@ def to_position(self, coordinate: int) -> Point: def to_coordinate(self, point: Point) -> int: """Convert a locus point dataclass to a coordinate. - :arg Point point: Locus point dataclass. + :arg Point point: Locus point dataclass, its position relative to this + locus. :returns int: Coordinate. + + :raises ValueError: If a non-zero offset is not at a locus boundary, or + the point converts to a negative coordinate. + :raises IndexError: If the position exceeds the locus length, or the + offset is at the wrong boundary. """ self._validate_point(point.position, point.offset) if self._inverted: - return self.boundary[1] - point.position - point.offset - return self.boundary[0] + point.position + point.offset + coordinate = self.boundary[1] - point.position - point.offset + else: + coordinate = self.boundary[0] + point.position + point.offset + if coordinate < 0: + raise ValueError( + f'Position {point.position} with offset {point.offset} converts to ' + f'negative coordinate {coordinate}.' + ) + + return coordinate diff --git a/tests/test_locus.py b/tests/test_locus.py index 451be6d..dd85a54 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -79,6 +79,17 @@ def test_invalid_locus_coordinate(): assert str(error.value) == 'Value must be an integer.' +def test_locus_negative_coordinate(): + """An offset may not convert to a coordinate before the reference start.""" + with pytest.raises(ValueError) as error: + Locus((0, 10)).to_coordinate(Point(position=0, offset=-1)) + assert str(error.value) == 'Position 0 with offset -1 converts to negative coordinate -1.' + + with pytest.raises(ValueError) as error: + Locus((0, 10), True).to_coordinate(Point(position=9, offset=1)) + assert str(error.value) == 'Position 9 with offset 1 converts to negative coordinate -1.' + + def test_invalid_locus_point(): """Forward orientent Locus with invalid point.""" locus = Locus((30, 35)) From 43c5ccb89ab073f431531bd55257cc33fa2301b2 Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Tue, 8 Sep 2026 13:05:10 +0200 Subject: [PATCH 220/236] Document the multi locus module and remove types from docstrings. --- mutalyzer_crossmapper/multi_locus.py | 145 +++++++++++++++++++++++---- 1 file changed, 123 insertions(+), 22 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 53576cd..dee5c98 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -1,3 +1,11 @@ +"""Conversions between coordinates and points for multiple loci. + +The loci are typically the exons of a transcript, with the gaps between them +its introns. They must be sorted and must not overlap, both of which are +checked on construction. Zero-based positions run continuously across them, +as if the loci were joined end to end. A coordinate that falls in a gap +between two loci is expressed as an offset from the nearest locus boundary. +""" from bisect import bisect_right from itertools import accumulate from dataclasses import dataclass @@ -9,7 +17,16 @@ @dataclass(slots=True) class Point(LocusPoint): - """Point dataclass""" + """A position within multiple loci, with an offset and a region. + + The region is ``'u'`` upstream of the joined loci and ``'d'`` downstream, + and empty everywhere in between, including in a gap between two loci. + Upstream is at the lower coordinates only when not inverted. + + :arg position: Zero-based position within the concatenated loci. + :arg offset: Offset relative to a locus boundary. + :arg region: One of ``''``, ``'u'`` or ``'d'``. + """ region: str = '' @@ -59,16 +76,59 @@ def _check_multi_locus(locations: list[tuple[int, int]], length: int | None = No def _offsets(locations: list[tuple[int, int]], orientation: int) -> list[int]: """For each location, calculate the length of the preceding locations. - :arg list locations: List of locations. - :arg int orientation: Direction of locations. + :arg locations: List of locations. + :arg orientation: Direction of locations. - :returns list: List of cumulative location lengths. + :returns: List of cumulative location lengths. """ return [0] + list(accumulate(map(lambda x: x[1] - x[0], locations[::orientation][:-1]))) class MultiLocus(): - """MultiLocus object.""" + """Convert coordinates to and from points across multiple loci. + + For ``MultiLocus([(10, 13), (16, 19)])``, positions 0 to 2 cover the first + locus and 3 to 5 the second. A coordinate in a gap between two loci takes + an offset from whichever locus boundary is nearest. When a gap is of odd + length, its middle coordinate is arbitrarily given a positive offset:: + + coordinate 9 10 11 12 13 14 15 16 17 18 19 + | | | | | | | | | | | + point position 0 0 1 2 2 2 3 3 4 5 5 + offset -1 0 0 0 1 2 -1 0 0 0 1 + region u '' '' '' '' '' '' '' '' '' d + + >>> from mutalyzer_crossmapper.multi_locus import MultiLocus, Point + >>> multi_locus = MultiLocus([(10, 13), (16, 19)]) + >>> multi_locus.to_position(14) + Point(position=2, offset=2, region='') + >>> multi_locus.to_coordinate(Point(position=2, offset=2, region='')) + 14 + + Only the offset staying within the gap is enforced, so a coordinate + there can also be described from the other locus. Converting back always + gives the nearest boundary form. + + >>> multi_locus.to_coordinate(Point(position=3, offset=-2, region='')) + 14 + >>> multi_locus.to_coordinate(Point(position=3, offset=-4, region='')) + Traceback (most recent call last): + IndexError: Offset -4 exceeds intron length. + + For loci on the reverse-complement strand, set ``inverted=True``:: + + coordinate 9 10 11 12 13 14 15 16 17 18 19 + | | | | | | | | | | | + point position 5 5 4 3 3 2 2 2 1 0 0 + offset 1 0 0 0 -1 2 1 0 0 0 -1 + region d '' '' '' '' '' '' '' '' '' u + + >>> inverted = MultiLocus([(10, 13), (16, 19)], inverted=True) + >>> inverted.to_position(14) + Point(position=2, offset=2, region='') + >>> inverted.to_position(18) + Point(position=0, offset=0, region='') + """ def __init__( self, @@ -76,10 +136,18 @@ def __init__( inverted: bool = False, length: int | None = None, ) -> None: - """ - :arg list locations: List of locus locations. - :arg bool inverted: Orientation. - :arg int|None length: Length of the reference sequence, None if unknown. + """Initialize a MultiLocus object. + + :arg locations: List of half-open intervals, sorted ascending and + non-overlapping. + :arg inverted: Orientation. + :arg length: Length of the reference sequence, None if unknown. The + last location may end exactly at it. + + :raises ValueError: If the locations are not a non-empty list of + valid loci, are out of order or overlapping, if the orientation + is not a boolean, or if the length is not a positive integer the + last location fits in. """ _check_multi_locus(locations, length) if not isinstance(inverted, bool): @@ -95,17 +163,31 @@ def __init__( self._end = sum(end - start for start, end in locations) def _validate_coord(self, coordinate: int) -> None: - """Check if the coordinate is valid.""" + """Check that the coordinate is within the reference sequence. + + Without a reference length there is no upper bound to check against. + """ if self._length is not None: _check_coordinate_within_length(coordinate, self._length) def _validate_point(self, index: int, position: int, offset: int, region: str) -> None: - """Validate if a multi locus Point is valid under HGVS rules. - - :arg int index: Index of the locus. - :arg int position: Position. - :arg int offset: Offset. - :arg str region: Region. + """Check that a point is described from the right side of the loci. + + In the ``'u'`` and ``'d'`` regions the position must be the outermost + one and the offset must point away from the loci. Elsewhere the offset + must stay within the gap between two loci that it reaches into, and a + point on the outer edge of the first or last locus belongs to ``'u'`` + or ``'d'`` instead. + + :arg index: Index of the locus. + :arg position: Position. + :arg offset: Offset. + :arg region: Region. + + :raises ValueError: If the position is not the outermost one for its + region, the offset has the wrong sign, or it reaches beyond the + reference sequence. + :raises IndexError: If the offset reaches beyond the intron. """ if region == 'u': if position != 0: @@ -183,6 +265,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - raise IndexError(f'Offset {offset} exceeds intron length.') def _direction(self, index: int) -> int: + """Convert a locus index between position and coordinate order.""" if self._inverted: return len(self._offsets) - index - 1 return index @@ -190,9 +273,9 @@ def _direction(self, index: int) -> int: def _outside(self, coordinate: int) -> int: """Calculate the offset relative to this MultiLocus. - :arg int coordinate: Coordinate. + :arg coordinate: Coordinate. - :returns int: Negative: upstream, 0: inside, positive: downstream. + :returns: Negative: upstream, 0: inside, positive: downstream. """ if coordinate < self._loci[0].boundary[0]: return coordinate - self._loci[0].boundary[0] @@ -203,9 +286,16 @@ def _outside(self, coordinate: int) -> int: def to_position(self, coordinate: int) -> Point: """Convert a coordinate to a multi locus point dataclass. - :arg int coordinate: Coordinate. + A coordinate in a gap between two loci is always given the nearest + boundary form, and one outside the loci altogether the ``'u'`` or + ``'d'`` region. + + :arg coordinate: Zero-based coordinate, non-negative. - :returns Point: Multi locus point dataclass. + :returns: Multi locus point dataclass. + + :raises ValueError: If the coordinate is not a non-negative integer, or + lies beyond the reference sequence. """ _check_non_negative_int(coordinate) self._validate_coord(coordinate) @@ -223,9 +313,20 @@ def to_position(self, coordinate: int) -> Point: def to_coordinate(self, point: Point) -> int: """Convert a multi locus point dataclass to a coordinate. - :arg Point point: Multi locus point dataclass. + A coordinate in a gap between two loci can be described from either + flanking locus and both forms are accepted, so converting the result + back with ``to_position`` does not always give the original point. + + :arg point: Multi locus point dataclass, its position relative to the + concatenated loci. + + :returns: Coordinate. - :returns int: Coordinate. + :raises ValueError: If the position is not the outermost one for its + region, the offset has the wrong sign, or it reaches beyond the + reference sequence. + :raises IndexError: If the offset reaches beyond the intron, or the + position exceeds the length of the loci. """ index = min( len(self._offsets), max(0, bisect_right(self._offsets, point.position) - 1) From 7daf35bd726407556929bf78ce7da257b92157b4 Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Wed, 9 Sep 2026 09:30:03 +0200 Subject: [PATCH 221/236] Remove types from docs. --- mutalyzer_crossmapper/locus.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 76ae001..2fa4bde 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -1,7 +1,7 @@ """Conversions between coordinates and points for a single locus. A locus is a half-open interval ``[start, end)`` defined on a sequence. -It can be addressed either by a coordinate, which is a zero-based index into +It can be described either by a coordinate, which is a zero-based index into that sequence, or by a point, which is a zero-based position within the locus plus an offset relative to its start or end. """ @@ -35,8 +35,8 @@ class Point: The offset is zero for positions inside the locus. A non-zero offset is only valid at a boundary: negative at the start, positive at the end. - :arg int position: Zero-based position within the locus. - :arg int offset: Offset relative to the locus start or end. + :arg position: Zero-based position within the locus. + :arg offset: Offset relative to the locus start or end. """ position: int offset: int = 0 @@ -97,9 +97,9 @@ class Locus(): def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: """Initialize a Locus object. - :arg tuple location: Half-open interval, with the start strictly - smaller than the end. - :arg bool inverted: Orientation. + :arg location: Half-open interval, with the start strictly smaller + than the end. + :arg inverted: Orientation. :raises ValueError: If the location is not a tuple of two non-negative integers, or its start is not smaller than its end. @@ -131,9 +131,9 @@ def to_position(self, coordinate: int) -> Point: Coordinates outside the locus are converted to a boundary position with a non-zero offset. - :arg int coordinate: Zero-based coordinate, non-negative. + :arg coordinate: Zero-based coordinate, non-negative. - :returns Point: Locus point dataclass. + :returns: Locus point dataclass. :raises ValueError: If the coordinate is not a non-negative integer. """ @@ -155,10 +155,9 @@ def to_position(self, coordinate: int) -> Point: def to_coordinate(self, point: Point) -> int: """Convert a locus point dataclass to a coordinate. - :arg Point point: Locus point dataclass, its position relative to this - locus. + :arg point: Locus point dataclass, its position relative to this locus. - :returns int: Coordinate. + :returns: Coordinate. :raises ValueError: If a non-zero offset is not at a locus boundary, or the point converts to a negative coordinate. From faf89df795c3c84eb4108ac8cdaaef5d312c2a17 Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Wed, 9 Sep 2026 15:55:40 +0200 Subject: [PATCH 222/236] Document the crossmapper module. --- mutalyzer_crossmapper/crossmapper.py | 351 +++++++++++++++++++++++---- 1 file changed, 304 insertions(+), 47 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 55b6996..97418d7 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -1,3 +1,12 @@ +"""Conversions between coordinates and points in the HGVS numbering systems. + +A coordinate is the zero-based index of a nucleotide in the reference +sequence. A point has a position in one of four numbering systems, genomic +(g./m./o.), noncoding (n./r.), coding (c.) or protein (p.), each +represented by its own dataclass. Outside the genomic system a point also +has an offset and a region. Conversions between numbering systems should be +done via a coordinate. +""" from dataclasses import dataclass from .multi_locus import ( @@ -12,7 +21,10 @@ @dataclass(slots=True) class GenomicPoint: - """Genomic dataclass.""" + """A position in the genomic numbering system (g./m./o.). + + :arg position: One-based position in the reference sequence, positive. + """ position: int def __post_init__(self) -> None: @@ -25,14 +37,34 @@ def __str__(self) -> str: class Genomic(): - """Genomic crossmap object.""" + """Convert coordinates to and from genomic points (g./m./o.). + + The HGVS genomic numbering system runs over the reference sequence + itself, one-based, so a conversion is a shift of one:: + + coordinate 0 1 2 3 4 + | | | | | + point position 1 2 3 4 5 + + >>> from mutalyzer_crossmapper import Genomic, GenomicPoint + >>> crossmap = Genomic() + >>> crossmap.coordinate_to_genomic(0) + GenomicPoint(position=1) + >>> crossmap.genomic_to_coordinate(GenomicPoint(position=1)) + 0 + """ + def coordinate_to_genomic(self, coordinate: int, length: int | None = None) -> GenomicPoint: """Convert a coordinate to a genomic point dataclass (g./m./o.). - :arg int coordinate: Coordinate. - :arg int|None length: Length of the sequence. + :arg coordinate: Zero-based coordinate, non-negative. + :arg length: Length of the reference sequence, None if unknown. + + :returns: Genomic point dataclass. - :returns GenomicPoint: Genomic point dataclass. + :raises ValueError: If the coordinate is not a non-negative integer, + if the length is not a positive integer, or if the coordinate + goes outside of the reference sequence length. """ _check_non_negative_int(coordinate) if length is not None: @@ -43,16 +75,28 @@ def coordinate_to_genomic(self, coordinate: int, length: int | None = None) -> G def genomic_to_coordinate(self, point: GenomicPoint) -> int: """Convert a genomic point dataclass (g./m./o.) to a coordinate. - :arg GenomicPoint point: Genomic point dataclass. + :arg point: Genomic point dataclass. - :returns int: Coordinate. + :returns: Coordinate. """ return point.position - 1 @dataclass(slots=True) class NonCodingPoint(GenomicPoint): - """NonCoding dataclass.""" + """A position in the noncoding numbering system (n./r.), with an offset + and a region. + + A non-zero offset counts nucleotides from an exon boundary, into an + intron or beyond the transcript. Within an intron either boundary may be + used, but converting from a coordinate always gives the nearest one. The + region is ``'u'`` upstream of the transcript and ``'d'`` downstream of + it, and empty within it. + + :arg position: One-based transcript position, positive. + :arg offset: Offset in nucleotides, with respect to the transcript. + :arg region: One of ``''``, ``'u'`` or ``'d'``. + """ offset: int = 0 region: str = '' @@ -77,7 +121,53 @@ def __str__(self) -> str: class NonCoding(Genomic): - """NonCoding crossmap object.""" + """Convert coordinates to and from noncoding points (n./r.). + + On top of the functionality provided by the ``Genomic`` class, this class + adds the noncoding numbering system. + + The positions run over the exons only, so an intron coordinate takes an + offset with respect to the nearest exon boundary. For a transcript with + exons ``[(5, 8), (11, 14)]``:: + + coordinate 4 5 6 7 8 9 10 11 12 13 14 + | | | | | | | | | | | + point position 1 1 2 3 3 3 4 4 5 6 6 + offset -1 0 0 0 1 2 -1 0 0 0 1 + region u '' '' '' '' '' '' '' '' '' d + + >>> from mutalyzer_crossmapper import NonCoding, NonCodingPoint + >>> crossmap = NonCoding([(5, 8), (11, 14)]) + + The HGVS position "g.9" (coordinate ``8``) is equivalent to position + "n.3+1". + + >>> crossmap.coordinate_to_noncoding(8) + NonCodingPoint(position=3, offset=1, region='') + >>> crossmap.noncoding_to_coordinate(NonCodingPoint(position=3, offset=1)) + 8 + + When the coordinate is upstream or downstream of the transcript, the + region denotes on which side it lies. This makes it possible to + distinguish between intronic positions and those outside of the + transcript. + + >>> crossmap.coordinate_to_noncoding(4) + NonCodingPoint(position=1, offset=-1, region='u') + + For a transcript that resides on the reverse complement strand, set + ``inverted=True``:: + + coordinate 4 5 6 7 8 9 10 11 12 13 14 + | | | | | | | | | | | + point position 6 6 5 4 4 3 3 3 2 1 1 + offset 1 0 0 0 -1 2 1 0 0 0 -1 + region d '' '' '' '' '' '' '' '' '' u + + >>> inverted = NonCoding([(5, 8), (11, 14)], inverted=True) + >>> inverted.coordinate_to_noncoding(8) + NonCodingPoint(position=4, offset=-1, region='') + """ def __init__( self, @@ -85,10 +175,17 @@ def __init__( inverted: bool = False, length: int | None = None, ) -> None: - """ - :arg list locations: List of locus locations. - :arg bool inverted: Orientation. - :arg int|None length: Length of the reference sequence. + """Initialize a NonCoding object. + + :arg locations: List of exon locations as zero-based half-open + intervals, sorted ascending and non-overlapping. + :arg inverted: Orientation. + :arg length: Length of the reference sequence, None if unknown. + + :raises ValueError: If the locations are not a non-empty list of + valid exons, are out of order or overlapping, if the orientation + is not a boolean, or if the length is not a positive integer + large enough to contain the exons. """ _check_multi_locus(locations, length) self._inverted = inverted @@ -97,9 +194,12 @@ def __init__( def coordinate_to_noncoding(self, coordinate: int) -> NonCodingPoint: """Convert a coordinate to a noncoding point dataclass (n./r.). - :arg int coordinate: Coordinate. + :arg coordinate: Zero-based coordinate, non-negative. - :returns NonCodingPoint: Noncoding point dataclass. + :returns: Noncoding point dataclass. + + :raises ValueError: If the coordinate is not a non-negative integer, + or goes outside of the reference sequence length. """ point = self._noncoding.to_position(coordinate) return NonCodingPoint( @@ -111,9 +211,16 @@ def coordinate_to_noncoding(self, coordinate: int) -> NonCodingPoint: def noncoding_to_coordinate(self, point: NonCodingPoint) -> int: """Convert a noncoding point dataclass (n./r.) to a coordinate. - :arg NonCodingPoint point: Noncoding point dataclass. + :arg point: Noncoding point dataclass. + + :returns: Coordinate. - :returns int: Coordinate. + :raises ValueError: If the position is not at the boundary its region + requires, or the offset has the wrong sign or reaches beyond the + reference sequence length. + :raises IndexError: If the offset reaches beyond the intron, the + offset is at the wrong boundary, or the position exceeds the + length of the transcript. """ try: return self._noncoding.to_coordinate( @@ -135,13 +242,30 @@ def noncoding_to_coordinate(self, point: NonCodingPoint) -> int: @dataclass(slots=True) class CodingPoint(NonCodingPoint): - """Coding dataclass.""" + """A position in the coding numbering system (c.), with an offset and + a region. + + The region denotes the location of the position with respect to the CDS. + This is needed in order to work with the HGVS "-" and "*" positions. + + :arg position: One-based position within its region, positive. + :arg offset: Offset in nucleotides, with respect to the transcript. + :arg region: ``'-'`` upstream of the CDS, ``''`` in the CDS, ``'*'`` + downstream of the CDS, or ``'u'`` and ``'d'`` outside the transcript. + """ allowed_regions = ['', 'u', 'd', '-', '*'] @dataclass(slots=True) class ProteinPoint(CodingPoint): - """Protein dataclass.""" + """A codon position in the protein numbering system (p.), with an offset + and a region. + + :arg position: One-based codon position, positive. + :arg offset: Offset in nucleotides, with respect to the transcript. + :arg region: As for a coding position. + :arg position_in_codon: Position within the codon, 1, 2 or 3. + """ position_in_codon: int = 1 def __post_init__(self) -> None: @@ -157,7 +281,46 @@ def __str__(self) -> str: class Coding(NonCoding): - """Coding crossmap object.""" + """Convert coordinates to and from coding points (c.). + + On top of the functionality provided by the ``NonCoding`` class, this + class adds the coding and the protein numbering systems. The positions + are counted from the start of the CDS, and the region records whether a + position lies before it, in it or after it. For a transcript with exons + ``[(5, 8), (11, 14)]`` and CDS ``(6, 12)``:: + + coordinate 4 5 6 7 8 9 10 11 12 13 14 + | | | | | | | | | | | + point position 1 1 1 2 2 2 3 3 1 2 2 + offset -1 0 0 0 1 2 -1 0 0 0 1 + region u - '' '' '' '' '' '' * * d + + >>> from mutalyzer_crossmapper import Coding, CodingPoint + >>> crossmap = Coding([(5, 8), (11, 14)], (6, 12)) + + The HGVS position "g.6" (coordinate ``5``) is equivalent to position + "c.-1". + + >>> point = crossmap.coordinate_to_coding(5) + >>> point + CodingPoint(position=1, offset=0, region='-') + >>> crossmap.coding_to_coordinate(point) + 5 + + Likewise, the HGVS position "g.13" (coordinate ``12``) is equivalent to + position "c.*1". + + >>> crossmap.coordinate_to_coding(12) + CodingPoint(position=1, offset=0, region='*') + + The CDS spans a single codon here, so its three nucleotides are at the + coordinates ``6``, ``7`` and ``11``, the last one on the other side of + the intron. + + >>> crossmap.coordinate_to_protein(11) + ProteinPoint(position=1, offset=0, region='', position_in_codon=3) + """ + def __init__( self, locations: list[tuple[int, int]], @@ -165,11 +328,20 @@ def __init__( inverted: bool = False, length: int|None = None ) -> None: - """ - :arg list locations: List of locus locations. - :arg tuple cds: Locus location. - :arg bool inverted: Orientation. - :arg int|None length: Length of the reference sequence. + """Initialize a Coding object. + + :arg locations: List of exon locations as zero-based half-open + intervals, sorted ascending and non-overlapping. + :arg cds: Location of the CDS as a zero-based half-open interval, so + its end is the coordinate after its last nucleotide. + :arg inverted: Orientation. + :arg length: Length of the reference sequence, None if unknown. + + :raises ValueError: If the locations are not a non-empty list of + valid exons, are out of order or overlapping, if the CDS is not + a valid location within the exons, if the orientation is not a + boolean, or if the length is not a positive integer large enough + to contain the exons and the CDS. """ NonCoding.__init__(self, locations, inverted=inverted, length=length) self._check_cds(cds, locations, length) @@ -204,7 +376,12 @@ def _check_cds( locations: list[tuple[int, int]], length: int | None = None, ) -> None: - """Check if the CDS is valid.""" + """Check if the CDS is valid. + + Each end must lie between the start and the end of the exon nearest + to it. Both the CDS and the exons are half-open, so a CDS ending at + the last nucleotide of an exon has the same end as that exon. + """ _check_locus(cds) if length is not None: _check_location_end_within_length(cds[1], length) @@ -216,8 +393,18 @@ def _check_cds( def _validate_point(self, position: int, region: str) -> None: """Validate a coding point model under HGVS rules. - :arg int position: Position. - :arg str region: Region. + A position must stay within the stretch of the transcript that its + region numbers. The ``'u'`` and ``'d'`` regions lie outside the + transcript, so a position there anchors at the one its first or last + nucleotide carries. Position ``1`` is accepted as well, which is what + the degenerate correction gives when the neighbouring UTR is absent. + + :arg position: Position. + :arg region: Region. + + :raises ValueError: If the position is beyond the ``'-'``, ``''`` or + ``'*'`` region it is in, or is not an anchor for the ``'u'`` or + ``'d'`` region. """ if region == 'u': if position not in (1, self._coding[0]): @@ -243,11 +430,14 @@ def _validate_point(self, position: int, region: str) -> None: def _coordinate_to_coding(self, coordinate: int) -> CodingPoint: - """Convert a coordinate to a coding point dataclass (c./r.). + """Convert a coordinate to a coding point dataclass (c.). - :arg int coordinate: Coordinate. + A coordinate outside the transcript is given the ``'u'`` or ``'d'`` + region, leaving the degenerate point to ``coordinate_to_coding``. - :returns CodingPoint: Coding point dataclass (c./r.). + :arg coordinate: Zero-based coordinate, non-negative. + + :returns: Coding point dataclass (c.). """ noncoding_point = self._noncoding.to_position(coordinate) @@ -277,12 +467,34 @@ def _coordinate_to_coding(self, coordinate: int) -> CodingPoint: return CodingPoint(position=position, offset=offset, region=region) def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> CodingPoint: - """Convert a coordinate to a coding point dataclass (c./r.). - - :arg int coordinate: Coordinate. - :arg bool degenerate: Return a degenerate coding point dataclass. - - :returns CodingPoint: Coding point dataclass (c./r.). + """Convert a coordinate to a coding point dataclass (c.). + + A coordinate outside the transcript is given the ``'u'`` or ``'d'`` + region. With ``degenerate`` set it is expressed in the ``'-'`` or + ``'*'`` region instead, by counting on past the end of the + transcript, so only the outermost columns of the table above + change:: + + coordinate 4 5 6 7 8 9 10 11 12 13 14 + | | | | | | | | | | | + point position 2 1 1 2 2 2 3 3 1 2 3 + offset 0 0 0 0 1 2 -1 0 0 0 0 + region - - '' '' '' '' '' '' * * * + + >>> from mutalyzer_crossmapper import Coding + >>> crossmap = Coding([(5, 8), (11, 14)], (6, 12)) + >>> crossmap.coordinate_to_coding(4, degenerate=True) + CodingPoint(position=2, offset=0, region='-') + >>> crossmap.coordinate_to_coding(14, degenerate=True) + CodingPoint(position=3, offset=0, region='*') + + :arg coordinate: Zero-based coordinate, non-negative. + :arg degenerate: Return a degenerate coding point dataclass. + + :returns: Coding point dataclass (c.). + + :raises ValueError: If the coordinate is not a non-negative integer, + or goes outside of the reference sequence length. """ point = self._coordinate_to_coding(coordinate) @@ -305,11 +517,14 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> Cod return point def _coding_to_coordinate(self, point: CodingPoint) -> int: - """Convert a coding point dataclass (c./r.) to a coordinate. + """Convert a coding point dataclass (c.) to a coordinate. - :arg CodingPoint point: Coding point dataclass (c./r.). + A degenerate point is rejected rather than corrected, so the + position must stay within the region it names. - :returns int: Coordinate. + :arg point: Coding point dataclass (c.). + + :returns: Coordinate. """ region = point.region position = point.position @@ -346,11 +561,21 @@ def _coding_to_coordinate(self, point: CodingPoint) -> int: raise def coding_to_coordinate(self, point: CodingPoint) -> int: - """Convert a coding point dataclass (c./r.) to a coordinate. + """Convert a coding point dataclass (c.) to a coordinate. + + A degenerate point, one with offset zero whose position counts on + past the end of the transcript in the ``'-'`` or ``'*'`` region, is + silently corrected. + + :arg point: Coding point dataclass (c.). - :arg CodingPoint point: Coding point dataclass (c./r.). + :returns: Coordinate. - :returns int: Coordinate. + :raises ValueError: If the position is not in the region it claims, + a non-zero offset is not at an exon boundary, or the offset has + the wrong sign or reaches beyond the reference sequence. + :raises IndexError: If the offset reaches beyond the intron, or the + offset is at the wrong boundary. """ # Silently correct for degenerate points if point.offset == 0: @@ -384,9 +609,32 @@ def coding_to_coordinate(self, point: CodingPoint) -> int: def coordinate_to_protein(self, coordinate: int) -> ProteinPoint: """Convert a coordinate to a protein point dataclass (p.). - :arg int coordinate: Coordinate. + The position is that of the codon, with ``position_in_codon`` + selecting one of its three nucleotides. A codon may be split by an + intron. For a transcript with exons ``[(5, 8), (11, 17)]`` and CDS + ``(6, 15)``, the first codon is at the coordinates ``6``, ``7`` and + ``11``:: - :returns ProteinPoint: Protein point dataclass (p.). + coordinate 5 6 7 8 9 10 11 12 13 14 15 + | | | | | | | | | | | + point position 1 1 1 1 1 1 1 2 2 2 1 + offset 0 0 0 1 2 -1 0 0 0 0 0 + region - '' '' '' '' '' '' '' '' '' * + in codon 3 1 2 2 2 3 3 1 2 3 1 + + >>> from mutalyzer_crossmapper import Coding + >>> crossmap = Coding([(5, 8), (11, 17)], (6, 15)) + >>> crossmap.coordinate_to_protein(11) + ProteinPoint(position=1, offset=0, region='', position_in_codon=3) + >>> crossmap.coordinate_to_protein(12) + ProteinPoint(position=2, offset=0, region='', position_in_codon=1) + + :arg coordinate: Zero-based coordinate, non-negative. + + :returns: Protein point dataclass (p.). + + :raises ValueError: If the coordinate is not a non-negative integer, + or goes outside of the reference sequence length. """ point = self.coordinate_to_coding(coordinate) @@ -408,9 +656,18 @@ def coordinate_to_protein(self, coordinate: int) -> ProteinPoint: def protein_to_coordinate(self, point: ProteinPoint) -> int: """Convert a protein point dataclass (p.) to a coordinate. - :arg ProteinPoint point: Protein point dataclass (p.). + A degenerate point is silently corrected, as in + ``coding_to_coordinate``. + + :arg point: Protein point dataclass (p.). + + :returns: Coordinate. - :returns int: Coordinate. + :raises ValueError: If the position is not in the region it claims, + a non-zero offset is not at an exon boundary, or the offset has + the wrong sign or reaches beyond the reference sequence. + :raises IndexError: If the offset reaches beyond the intron, or the + offset is at the wrong boundary. """ if point.region in ('-', 'u'): position = 3 * point.position - point.position_in_codon + 1 From 1fda7b845d881de32e6b85dd0b5b1532e8d5a454 Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Wed, 9 Sep 2026 16:07:53 +0200 Subject: [PATCH 223/236] Reject unsupported types for the orientation and the position in codon. --- mutalyzer_crossmapper/crossmapper.py | 3 ++- mutalyzer_crossmapper/locus.py | 7 +++++-- tests/test_crossmapper.py | 3 +++ tests/test_locus.py | 3 +++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 97418d7..6c4e259 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -271,7 +271,8 @@ class ProteinPoint(CodingPoint): def __post_init__(self) -> None: CodingPoint.__post_init__(self) - if not isinstance(self.position_in_codon, int) or self.position_in_codon not in (1, 2, 3): + _check_int(self.position_in_codon) + if self.position_in_codon not in (1, 2, 3): raise ValueError('Position_in_codon must be 1, 2, or 3.') def __str__(self) -> str: diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 2fa4bde..3eecde5 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -80,7 +80,7 @@ class Locus(): >>> locus.to_coordinate(Point(position=4, offset=1)) 15 - For a locus on the reverse-complement strand, set ``inverted=True``:: + For a locus on the reverse complement strand, set ``inverted=True``:: coordinate 8 9 10 11 12 13 14 15 16 | | | | | | | | | @@ -102,9 +102,12 @@ def __init__(self, location: tuple[int, int], inverted: bool = False) -> None: :arg inverted: Orientation. :raises ValueError: If the location is not a tuple of two non-negative - integers, or its start is not smaller than its end. + integers, its start is not smaller than its end, or the + orientation is not a boolean. """ _check_locus(location) + if not isinstance(inverted, bool): + raise ValueError(f'Value {inverted} is not a boolean.') self._inverted = inverted self.boundary = location[0], location[1] - 1 diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index be321a8..328a3ee 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -1491,6 +1491,9 @@ def test_Coding_protein_point_invalid_initialization(): with pytest.raises(ValueError) as error: ProteinPoint(position=1, offset=0, region='', position_in_codon=0) assert str(error.value) == 'Position_in_codon must be 1, 2, or 3.' + with pytest.raises(ValueError) as error: + ProteinPoint(position=1, offset=0, region='', position_in_codon=True) + assert str(error.value) == 'Value must be an integer.' def test_Coding_protein(): diff --git a/tests/test_locus.py b/tests/test_locus.py index dd85a54..82c914f 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -30,6 +30,9 @@ def test_invalid_locus_initialization(): with pytest.raises(ValueError) as error: Locus((10, 10)) assert str(error.value) == 'Locus start 10 must be smaller than locus end 10.' + with pytest.raises(ValueError) as error: + Locus((10, 20), 100) + assert str(error.value) == 'Value 100 is not a boolean.' # Inverted Locus initialization with pytest.raises(ValueError) as error: From 5326b002bb8952ef3a00a2f87956ab990e480962 Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Thu, 10 Sep 2026 10:13:08 +0200 Subject: [PATCH 224/236] Be more specific when checking the CDS. --- mutalyzer_crossmapper/crossmapper.py | 49 ++++++++++++++++++++-------- tests/test_crossmapper.py | 36 +++++++++++++++++--- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 6c4e259..285cedb 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -333,16 +333,18 @@ def __init__( :arg locations: List of exon locations as zero-based half-open intervals, sorted ascending and non-overlapping. - :arg cds: Location of the CDS as a zero-based half-open interval, so - its end is the coordinate after its last nucleotide. + :arg cds: Location of the CDS as a zero-based half-open interval, + with each end within an exon or at one of its boundaries. The end + of one exon and the start of the next describe the same CDS. :arg inverted: Orientation. :arg length: Length of the reference sequence, None if unknown. :raises ValueError: If the locations are not a non-empty list of - valid exons, are out of order or overlapping, if the CDS is not - a valid location within the exons, if the orientation is not a - boolean, or if the length is not a positive integer large enough - to contain the exons and the CDS. + valid exons, are out of order or overlapping, if either CDS + boundary lies outside the transcript or strictly inside an + intron, if the orientation is not a boolean, or if the length is + not a positive integer large enough to contain the exons and the + CDS. """ NonCoding.__init__(self, locations, inverted=inverted, length=length) self._check_cds(cds, locations, length) @@ -379,17 +381,38 @@ def _check_cds( ) -> None: """Check if the CDS is valid. - Each end must lie between the start and the end of the exon nearest - to it. Both the CDS and the exons are half-open, so a CDS ending at - the last nucleotide of an exon has the same end as that exon. + Each CDS boundary must fall within an exon or coincide with an exon + boundary. Because the CDS and exon intervals are half-open, the end + of an exon and the start of the next exon represent the same + position in the spliced transcript. """ _check_locus(cds) if length is not None: _check_location_end_within_length(cds[1], length) - for coord in cds: - index = _nearest_location(locations, coord) - if coord < locations[index][0] or coord > locations[index][1]: - raise ValueError(f'Coordinate {coord} of CDS {cds} is not within any exon.') + + for coordinate in cds: + if coordinate < locations[0][0]: + raise ValueError( + f'CDS boundary {coordinate} of {cds} lies before the first ' + f'exon {locations[0]}.' + ) + if coordinate > locations[-1][1]: + raise ValueError( + f'CDS boundary {coordinate} of {cds} lies after the last ' + f'exon {locations[-1]}.' + ) + + index = _nearest_location(locations, coordinate) + start, end = locations[index] + if start <= coordinate <= end: + continue + + # The boundary is in the intron before or after the nearest exon. + left_index = index - 1 if coordinate < start else index + raise ValueError( + f'CDS boundary {coordinate} of {cds} lies inside an intron between ' + f'exons {locations[left_index]} and {locations[left_index + 1]}.' + ) def _validate_point(self, position: int, region: str) -> None: """Validate a coding point model under HGVS rules. diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 328a3ee..af58f83 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -487,10 +487,10 @@ def test_Coding_invalid(): assert str(error.value) == 'Locus start 20 must be smaller than locus end 20.' with pytest.raises(ValueError) as error: Coding([(10, 20)], (9,15)) - assert str(error.value) == 'Coordinate 9 of CDS (9, 15) is not within any exon.' + assert str(error.value) == 'CDS boundary 9 of (9, 15) lies before the first exon (10, 20).' with pytest.raises(ValueError) as error: Coding([(10, 20)], (10,21)) - assert str(error.value) == 'Coordinate 21 of CDS (10, 21) is not within any exon.' + assert str(error.value) == 'CDS boundary 21 of (10, 21) lies after the last exon (10, 20).' with pytest.raises(ValueError) as error: Coding([(10, 20)], (15, 10)) assert str(error.value) == 'Locus start 15 must be smaller than locus end 10.' @@ -498,16 +498,27 @@ def test_Coding_invalid(): Coding([], None) assert str(error.value) == 'Locations must be a non-empty list of tuples.' + # Ends that lie strictly between two exons. + with pytest.raises(ValueError) as error: + Coding([(5, 8), (11, 14)], (6, 9)) + assert str(error.value) == 'CDS boundary 9 of (6, 9) lies inside an intron between exons (5, 8) and (11, 14).' + with pytest.raises(ValueError) as error: + Coding([(5, 8), (11, 14)], (6, 10)) + assert str(error.value) == 'CDS boundary 10 of (6, 10) lies inside an intron between exons (5, 8) and (11, 14).' + with pytest.raises(ValueError) as error: + Coding([(5, 8), (11, 14), (20, 25)], (16, 22)) + assert str(error.value) == 'CDS boundary 16 of (16, 22) lies inside an intron between exons (11, 14) and (20, 25).' + # Reverse orientation with pytest.raises(ValueError) as error: Coding([(20, 20)], (20, 20), inverted=True) assert str(error.value) == 'Locus start 20 must be smaller than locus end 20.' with pytest.raises(ValueError) as error: Coding([(10, 20)], (9,15), inverted=True) - assert str(error.value) == 'Coordinate 9 of CDS (9, 15) is not within any exon.' + assert str(error.value) == 'CDS boundary 9 of (9, 15) lies before the first exon (10, 20).' with pytest.raises(ValueError) as error: Coding([(10, 20)], (10,21), inverted=True) - assert str(error.value) == 'Coordinate 21 of CDS (10, 21) is not within any exon.' + assert str(error.value) == 'CDS boundary 21 of (10, 21) lies after the last exon (10, 20).' with pytest.raises(ValueError) as error: Coding([(10, 20)], (15, 10), inverted=True) assert str(error.value) == 'Locus start 15 must be smaller than locus end 10.' @@ -516,6 +527,18 @@ def test_Coding_invalid(): assert str(error.value) == 'Locations must be a non-empty list of tuples.' +def test_Coding_cds_end_at_exon_start(): + """A CDS end is half-open, so it may be the start of the next exon.""" + exons = [(5, 8), (11, 14)] + # Ending where the second exon starts describes the same CDS as ending + # at the end of the first exon. + boundary = Coding(exons, (6, 11)) + exon_end = Coding(exons, (6, 8)) + for coordinate in (5, 6, 7, 11, 12, 13): + assert (boundary.coordinate_to_coding(coordinate) + == exon_end.coordinate_to_coding(coordinate)) + + def test_Coding_invalid_with_length(): """Raise ValueError if coordinate is out of bounds.""" with pytest.raises(ValueError) as error: @@ -530,6 +553,11 @@ def test_Coding_invalid_with_length(): Coding(_exons, _cds, length=0) assert str(error.value) == 'Value 0 is not positive.' + # The exons fit in the reference, the CDS end does not. + with pytest.raises(ValueError) as error: + Coding([(10, 20)], (12, 25), length=20) + assert str(error.value) == 'Location end 25 is inconsistent with reference length 20.' + def test_Coding(): """Forward oriented coding transcript.""" From c789c9619fdc95a5fc22094b1ec9683b71212e6d Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Thu, 10 Sep 2026 11:11:16 +0200 Subject: [PATCH 225/236] Accept downstream position 1 only when it can be converted. --- mutalyzer_crossmapper/crossmapper.py | 15 ++++++++------- tests/test_crossmapper.py | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 285cedb..98c37a6 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -420,8 +420,8 @@ def _validate_point(self, position: int, region: str) -> None: A position must stay within the stretch of the transcript that its region numbers. The ``'u'`` and ``'d'`` regions lie outside the transcript, so a position there anchors at the one its first or last - nucleotide carries. Position ``1`` is accepted as well, which is what - the degenerate correction gives when the neighbouring UTR is absent. + nucleotide carries. Position ``1`` is also accepted upstream, and + downstream when there is no 3' UTR. :arg position: Position. :arg region: Region. @@ -443,13 +443,14 @@ def _validate_point(self, position: int, region: str) -> None: if position not in range(1, self._exons[1] - self._coding[1] + 1): raise ValueError(f'Position {position} exceeds * region.') if region == 'd': - # Downstream positions anchor at the 3' UTR boundary, or at the - # last coding position when the 3' UTR is absent. + allowed: tuple[int, ...] if self._exons[1] == self._coding[1]: - downstream_boundary = self._coding[1] - self._coding[0] + # Without a 3' UTR the anchor is the last coding position, + # and the degenerate correction anchors at position 1. + allowed = (1, self._coding[1] - self._coding[0]) else: - downstream_boundary = self._exons[1] - self._coding[1] - if position not in (1, downstream_boundary): + allowed = (self._exons[1] - self._coding[1],) + if position not in allowed: raise ValueError(f'Position {position} is not in downstream boundary.') diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index af58f83..ca5c328 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -950,6 +950,16 @@ def test_Coding_no_utr3(): CodingPoint(position=5, offset=1, region='d'), ) + # Position 1 is also accepted downstream when there is no 3' UTR. + degenerate_equal( + crossmap.coding_to_coordinate, + 20, + [ + CodingPoint(position=1, offset=1, region='d'), + CodingPoint(position=1, offset=0, region='*'), + ], + ) + def test_Coding_no_utr3_unequal_utr5(): """Without a 3' UTR the downstream anchor is the last coding position.""" @@ -987,6 +997,16 @@ def test_Coding_no_utr3_inverted(): CodingPoint(position=5, offset=1, region='d'), ) + # Position 1 is also accepted downstream when there is no 3' UTR. + degenerate_equal( + crossmap.coding_to_coordinate, + 9, + [ + CodingPoint(position=1, offset=1, region='d'), + CodingPoint(position=1, offset=0, region='*'), + ], + ) + def test_Coding_no_utr3_inverted_unequal_utr5(): """Without a 3' UTR the downstream anchor is the last coding position.""" @@ -1410,6 +1430,9 @@ def test_Coding_invalid_position(): with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=1000, offset=2, region='d')) assert str(error.value) == 'Position 1000 is not in downstream boundary.' + with pytest.raises(ValueError) as error: + crossmap.coding_to_coordinate(CodingPoint(position=1, offset=2, region='d')) + assert str(error.value) == 'Position 1 is not in downstream boundary.' def test_Coding_inverted_invalid_position_inverted(): @@ -1446,6 +1469,9 @@ def test_Coding_inverted_invalid_position_inverted(): with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=1000, offset=2, region='d')) assert str(error.value) == 'Position 1000 is not in downstream boundary.' + with pytest.raises(ValueError) as error: + crossmap.coding_to_coordinate(CodingPoint(position=1, offset=2, region='d')) + assert str(error.value) == 'Position 1 is not in downstream boundary.' def test_Coding_invalid_offset(): From aa4a78ab322edc0fa316f1c0ad3f374a8d640de8 Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Thu, 10 Sep 2026 13:00:44 +0200 Subject: [PATCH 226/236] Report a degenerate position beyond the sequence as written. --- mutalyzer_crossmapper/crossmapper.py | 63 ++++++++++++++++------------ tests/test_crossmapper.py | 26 ++++++++++++ 2 files changed, 62 insertions(+), 27 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 98c37a6..3d6e16e 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -597,39 +597,42 @@ def coding_to_coordinate(self, point: CodingPoint) -> int: :returns: Coordinate. :raises ValueError: If the position is not in the region it claims, - a non-zero offset is not at an exon boundary, or the offset has - the wrong sign or reaches beyond the reference sequence. + a non-zero offset is not at an exon boundary, the offset has + the wrong sign, or the position or offset reaches beyond + the sequence. :raises IndexError: If the offset reaches beyond the intron, or the offset is at the wrong boundary. """ # Silently correct for degenerate points + corrected = None if point.offset == 0: - if point.region == '-' and point.position > self._coding[0]: - if self._coding[0] == 0: - return self._coding_to_coordinate(CodingPoint( - position=1, - offset=self._coding[0] - point.position, - region='u', - )) - return self._coding_to_coordinate(CodingPoint( - position=self._coding[0], - offset=self._coding[0] - point.position, + utr5_length = self._coding[0] + utr3_length = self._exons[1] - self._coding[1] + + if point.region == '-' and point.position > utr5_length: + corrected = CodingPoint( + position=utr5_length if utr5_length else 1, + offset=utr5_length - point.position, region='u', - )) - if point.region == '*' and point.position > self._exons[1] - self._coding[1]: - if self._exons[1] == self._coding[1]: - return self._coding_to_coordinate(CodingPoint( - position=1, - offset=point.position - (self._exons[1] - self._coding[1]), - region='d', - )) - return self._coding_to_coordinate(CodingPoint( - position=self._exons[1] - self._coding[1], - offset=point.position - (self._exons[1] - self._coding[1]), + ) + elif point.region == '*' and point.position > utr3_length: + corrected = CodingPoint( + position=utr3_length if utr3_length else 1, + offset=point.position - utr3_length, region='d', - )) + ) + + if corrected is None: + return self._coding_to_coordinate(point) - return self._coding_to_coordinate(point) + try: + return self._coding_to_coordinate(corrected) + except ValueError as error: + # The correction rewrote the point, so report what was given. + raise ValueError( + f'Position {point.position} of the {point.region} region ' + f'exceeds the sequence.' + ) from error def coordinate_to_protein(self, coordinate: int) -> ProteinPoint: """Convert a coordinate to a protein point dataclass (p.). @@ -684,13 +687,19 @@ def protein_to_coordinate(self, point: ProteinPoint) -> int: A degenerate point is silently corrected, as in ``coding_to_coordinate``. + Note that conversion and validation are delegated to + ``coding_to_coordinate``. Error messages may therefore report + the intermediate coding nucleotide position rather than + the supplied protein position. + :arg point: Protein point dataclass (p.). :returns: Coordinate. :raises ValueError: If the position is not in the region it claims, - a non-zero offset is not at an exon boundary, or the offset has - the wrong sign or reaches beyond the reference sequence. + a non-zero offset is not at an exon boundary, the offset has + the wrong sign, or the position or offset reaches beyond + the sequence. :raises IndexError: If the offset reaches beyond the intron, or the offset is at the wrong boundary. """ diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index ca5c328..f443cf2 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -1481,6 +1481,19 @@ def test_Coding_invalid_offset(): with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-6, region='u')) assert str(error.value) == 'Offset -6 exceeds upstream region.' + # A degenerate position beyond the sequence is reported as written. + with pytest.raises(ValueError) as error: + crossmap.coding_to_coordinate(CodingPoint(position=17, offset=0, region='-')) + assert str(error.value) == ( + 'Position 17 of the - region exceeds the sequence.') + + # The last sequence coordinate is valid; the next is out of bounds. + assert crossmap.coding_to_coordinate(CodingPoint(position=8, offset=0, region='*')) == 74 + with pytest.raises(ValueError) as error: + crossmap.coding_to_coordinate(CodingPoint(position=9, offset=0, region='*')) + assert str(error.value) == ( + 'Position 9 of the * region exceeds the sequence.') + with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=1, region='-')) assert str(error.value) == 'Position 1 is not at a locus boundary.' @@ -1511,6 +1524,19 @@ def test_Coding_invalid_offset_inverted(): with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-6, region='u')) assert str(error.value) == 'Offset -6 exceeds upstream region.' + # A degenerate position beyond the sequence is reported as written. + with pytest.raises(ValueError) as error: + crossmap.coding_to_coordinate(CodingPoint(position=17, offset=0, region='*')) + assert str(error.value) == ( + 'Position 17 of the * region exceeds the sequence.') + + # The last sequence coordinate is valid; the next is out of bounds. + assert crossmap.coding_to_coordinate(CodingPoint(position=8, offset=0, region='-')) == 74 + with pytest.raises(ValueError) as error: + crossmap.coding_to_coordinate(CodingPoint(position=9, offset=0, region='-')) + assert str(error.value) == ( + 'Position 9 of the - region exceeds the sequence.') + with pytest.raises(IndexError) as error: crossmap.coding_to_coordinate(CodingPoint(position=1, offset=1, region='-')) assert str(error.value) == 'Offset 1 should be at a locus end.' From 36ae40748a775ca3a7c091cf29e5416c8f4f7667 Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Thu, 10 Sep 2026 13:12:09 +0200 Subject: [PATCH 227/236] No more reference. --- mutalyzer_crossmapper/crossmapper.py | 34 ++++++++++++------------- mutalyzer_crossmapper/multi_locus.py | 26 +++++++++---------- tests/test_crossmapper.py | 38 ++++++++++++++-------------- tests/test_locus.py | 2 +- tests/test_multi_locus.py | 10 ++++---- 5 files changed, 55 insertions(+), 55 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 3d6e16e..9fb2a29 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -1,11 +1,11 @@ """Conversions between coordinates and points in the HGVS numbering systems. -A coordinate is the zero-based index of a nucleotide in the reference -sequence. A point has a position in one of four numbering systems, genomic -(g./m./o.), noncoding (n./r.), coding (c.) or protein (p.), each -represented by its own dataclass. Outside the genomic system a point also -has an offset and a region. Conversions between numbering systems should be -done via a coordinate. +A coordinate is the zero-based index of a nucleotide in the sequence. A +point has a position in one of four numbering systems, genomic (g./m./o.), +noncoding (n./r.), coding (c.) or protein (p.), each represented by its own +dataclass. Outside the genomic system a point also has an offset and a +region. Conversions between numbering systems should be done via a +coordinate. """ from dataclasses import dataclass @@ -23,7 +23,7 @@ class GenomicPoint: """A position in the genomic numbering system (g./m./o.). - :arg position: One-based position in the reference sequence, positive. + :arg position: One-based position in the sequence, positive. """ position: int @@ -39,7 +39,7 @@ def __str__(self) -> str: class Genomic(): """Convert coordinates to and from genomic points (g./m./o.). - The HGVS genomic numbering system runs over the reference sequence + The HGVS genomic numbering system runs over the sequence itself, one-based, so a conversion is a shift of one:: coordinate 0 1 2 3 4 @@ -58,13 +58,13 @@ def coordinate_to_genomic(self, coordinate: int, length: int | None = None) -> G """Convert a coordinate to a genomic point dataclass (g./m./o.). :arg coordinate: Zero-based coordinate, non-negative. - :arg length: Length of the reference sequence, None if unknown. + :arg length: Length of the sequence, None if unknown. :returns: Genomic point dataclass. :raises ValueError: If the coordinate is not a non-negative integer, if the length is not a positive integer, or if the coordinate - goes outside of the reference sequence length. + goes outside of the sequence length. """ _check_non_negative_int(coordinate) if length is not None: @@ -180,7 +180,7 @@ def __init__( :arg locations: List of exon locations as zero-based half-open intervals, sorted ascending and non-overlapping. :arg inverted: Orientation. - :arg length: Length of the reference sequence, None if unknown. + :arg length: Length of the sequence, None if unknown. :raises ValueError: If the locations are not a non-empty list of valid exons, are out of order or overlapping, if the orientation @@ -199,7 +199,7 @@ def coordinate_to_noncoding(self, coordinate: int) -> NonCodingPoint: :returns: Noncoding point dataclass. :raises ValueError: If the coordinate is not a non-negative integer, - or goes outside of the reference sequence length. + or goes outside of the sequence length. """ point = self._noncoding.to_position(coordinate) return NonCodingPoint( @@ -216,8 +216,8 @@ def noncoding_to_coordinate(self, point: NonCodingPoint) -> int: :returns: Coordinate. :raises ValueError: If the position is not at the boundary its region - requires, or the offset has the wrong sign or reaches beyond the - reference sequence length. + requires, or the offset has the wrong sign or reaches beyond + the sequence length. :raises IndexError: If the offset reaches beyond the intron, the offset is at the wrong boundary, or the position exceeds the length of the transcript. @@ -337,7 +337,7 @@ def __init__( with each end within an exon or at one of its boundaries. The end of one exon and the start of the next describe the same CDS. :arg inverted: Orientation. - :arg length: Length of the reference sequence, None if unknown. + :arg length: Length of the sequence, None if unknown. :raises ValueError: If the locations are not a non-empty list of valid exons, are out of order or overlapping, if either CDS @@ -519,7 +519,7 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> Cod :returns: Coding point dataclass (c.). :raises ValueError: If the coordinate is not a non-negative integer, - or goes outside of the reference sequence length. + or goes outside of the sequence length. """ point = self._coordinate_to_coding(coordinate) @@ -662,7 +662,7 @@ def coordinate_to_protein(self, coordinate: int) -> ProteinPoint: :returns: Protein point dataclass (p.). :raises ValueError: If the coordinate is not a non-negative integer, - or goes outside of the reference sequence length. + or goes outside of the sequence length. """ point = self.coordinate_to_coding(coordinate) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index dee5c98..cd9fdb0 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -39,18 +39,18 @@ def __post_init__(self) -> None: def _check_coordinate_within_length(value: int, length: int) -> None: - """Check if a zero-based coordinate is within reference length.""" + """Check if a zero-based coordinate is within sequence length.""" if value >= length: raise ValueError( - f'Coordinate {value} is not within the bounds of the reference length {length}.' + f'Coordinate {value} is not within the bounds of the sequence length {length}.' ) def _check_location_end_within_length(value: int, length: int) -> None: - """Check if a half-open interval end is within reference length.""" + """Check if a half-open interval end is within sequence length.""" if value > length: raise ValueError( - f'Location end {value} is inconsistent with reference length {length}.' + f'Location end {value} is inconsistent with sequence length {length}.' ) @@ -115,7 +115,7 @@ class MultiLocus(): Traceback (most recent call last): IndexError: Offset -4 exceeds intron length. - For loci on the reverse-complement strand, set ``inverted=True``:: + For loci on the reverse complement strand, set ``inverted=True``:: coordinate 9 10 11 12 13 14 15 16 17 18 19 | | | | | | | | | | | @@ -141,7 +141,7 @@ def __init__( :arg locations: List of half-open intervals, sorted ascending and non-overlapping. :arg inverted: Orientation. - :arg length: Length of the reference sequence, None if unknown. The + :arg length: Length of the sequence, None if unknown. The last location may end exactly at it. :raises ValueError: If the locations are not a non-empty list of @@ -163,9 +163,9 @@ def __init__( self._end = sum(end - start for start, end in locations) def _validate_coord(self, coordinate: int) -> None: - """Check that the coordinate is within the reference sequence. + """Check that the coordinate is within the sequence. - Without a reference length there is no upper bound to check against. + Without a sequence length there is no upper bound to check against. """ if self._length is not None: _check_coordinate_within_length(coordinate, self._length) @@ -185,8 +185,8 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - :arg region: Region. :raises ValueError: If the position is not the outermost one for its - region, the offset has the wrong sign, or it reaches beyond the - reference sequence. + region, the offset has the wrong sign, or it reaches beyond + the sequence. :raises IndexError: If the offset reaches beyond the intron. """ if region == 'u': @@ -295,7 +295,7 @@ def to_position(self, coordinate: int) -> Point: :returns: Multi locus point dataclass. :raises ValueError: If the coordinate is not a non-negative integer, or - lies beyond the reference sequence. + lies beyond the sequence. """ _check_non_negative_int(coordinate) self._validate_coord(coordinate) @@ -323,8 +323,8 @@ def to_coordinate(self, point: Point) -> int: :returns: Coordinate. :raises ValueError: If the position is not the outermost one for its - region, the offset has the wrong sign, or it reaches beyond the - reference sequence. + region, the offset has the wrong sign, or it reaches beyond + the sequence. :raises IndexError: If the offset reaches beyond the intron, or the position exceeds the length of the loci. """ diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index f443cf2..24f3d4b 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -46,7 +46,7 @@ def test_Genomic_invalid_with_length(): assert str(error.value) == 'Value must be non-negative.' with pytest.raises(ValueError) as error: crossmap.coordinate_to_genomic(99, 99) - assert str(error.value) == 'Coordinate 99 is not within the bounds of the reference length 99.' + assert str(error.value) == 'Coordinate 99 is not within the bounds of the sequence length 99.' with pytest.raises(ValueError) as error: crossmap.coordinate_to_genomic(0, 0) assert str(error.value) == 'Value 0 is not positive.' @@ -117,7 +117,7 @@ def test_NonCoding_invalid(): assert str(error.value) == 'Value must be an integer.' with pytest.raises(ValueError) as error: NonCoding(_exons, length=70) - assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' + assert str(error.value) == 'Location end 72 is inconsistent with sequence length 70.' # Reverse orientation with pytest.raises(ValueError) as error: @@ -134,19 +134,19 @@ def test_NonCoding_invalid(): assert str(error.value) == 'Value must be an integer.' with pytest.raises(ValueError) as error: NonCoding(_exons, inverted=True, length=70) - assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' + assert str(error.value) == 'Location end 72 is inconsistent with sequence length 70.' def test_NonCoding_invalid_with_length(): """Raise ValueError if coordinate is out of bounds.""" with pytest.raises(ValueError) as error: NonCoding(_exons, length=70) - assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' + assert str(error.value) == 'Location end 72 is inconsistent with sequence length 70.' # Reverse orientation with pytest.raises(ValueError) as error: NonCoding(_exons, inverted=True, length=70) - assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' + assert str(error.value) == 'Location end 72 is inconsistent with sequence length 70.' with pytest.raises(ValueError) as error: NonCoding(_exons, length=0) @@ -239,7 +239,7 @@ def test_NonCoding_with_length(): ) with pytest.raises(ValueError) as error: crossmap.coordinate_to_noncoding(75) - assert str(error.value) == 'Coordinate 75 is not within the bounds of the reference length 75.' + assert str(error.value) == 'Coordinate 75 is not within the bounds of the sequence length 75.' with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=22, offset=4, region='d')) assert str(error.value) == 'Offset 4 exceeds downstream region.' @@ -285,7 +285,7 @@ def test_NonCoding_inverted_with_length(): # Boundary between upstream and sequence end. with pytest.raises(ValueError) as error: crossmap.coordinate_to_noncoding(75) - assert str(error.value) == 'Coordinate 75 is not within the bounds of the reference length 75.' + assert str(error.value) == 'Coordinate 75 is not within the bounds of the sequence length 75.' with pytest.raises(ValueError) as error: crossmap.noncoding_to_coordinate(NonCodingPoint(position=1, offset=-4, region='u')) assert str(error.value) == 'Offset -4 exceeds upstream region.' @@ -441,8 +441,8 @@ def test_NonCoding_invalid_offset_inverted(): assert error.value.args[0] == 'Offset -1 at downstream boundary should be positive.' -def test_NonCoding_location_end_equal_reference_length(): - """Half-open exon end may equal reference length.""" +def test_NonCoding_location_end_equal_sequence_length(): + """Half-open exon end may equal sequence length.""" crossmap = NonCoding([(0, 10)], length=10) invariant( @@ -454,7 +454,7 @@ def test_NonCoding_location_end_equal_reference_length(): with pytest.raises(ValueError) as error: crossmap.coordinate_to_noncoding(10) - assert str(error.value) == 'Coordinate 10 is not within the bounds of the reference length 10.' + assert str(error.value) == 'Coordinate 10 is not within the bounds of the sequence length 10.' def test_CodingPoint_invalid_initialization(): @@ -543,20 +543,20 @@ def test_Coding_invalid_with_length(): """Raise ValueError if coordinate is out of bounds.""" with pytest.raises(ValueError) as error: Coding(_exons, _cds, length=70) - assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' + assert str(error.value) == 'Location end 72 is inconsistent with sequence length 70.' # Reverse orientation with pytest.raises(ValueError) as error: Coding(_exons, _cds, inverted=True, length=70) - assert str(error.value) == 'Location end 72 is inconsistent with reference length 70.' + assert str(error.value) == 'Location end 72 is inconsistent with sequence length 70.' with pytest.raises(ValueError) as error: Coding(_exons, _cds, length=0) assert str(error.value) == 'Value 0 is not positive.' - # The exons fit in the reference, the CDS end does not. + # The exons fit in the sequence, the CDS end does not. with pytest.raises(ValueError) as error: Coding([(10, 20)], (12, 25), length=20) - assert str(error.value) == 'Location end 25 is inconsistent with reference length 20.' + assert str(error.value) == 'Location end 25 is inconsistent with sequence length 20.' def test_Coding(): @@ -688,7 +688,7 @@ def test_Coding_with_length(): ) with pytest.raises(ValueError) as error: crossmap.coordinate_to_coding(75) - assert str(error.value) == 'Coordinate 75 is not within the bounds of the reference length 75.' + assert str(error.value) == 'Coordinate 75 is not within the bounds of the sequence length 75.' with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=5, offset=4, region='d')) @@ -761,7 +761,7 @@ def test_Coding_inverted_with_length(): # Boundary between upstream and sequence end. with pytest.raises(ValueError) as error: crossmap.coordinate_to_coding(75) - assert str(error.value) == 'Coordinate 75 is not within the bounds of the reference length 75.' + assert str(error.value) == 'Coordinate 75 is not within the bounds of the sequence length 75.' with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=5, offset=-4, region='u')) assert str(error.value) == 'Offset -4 exceeds upstream region.' @@ -1774,8 +1774,8 @@ def test_Coding_inverted_protein_degenerate(): ) -def test_Coding_cds_end_equal_reference_length(): - """Half-open exon/CDS end may equal reference length.""" +def test_Coding_cds_end_equal_sequence_length(): + """Half-open exon/CDS end may equal sequence length.""" crossmap = Coding([(0, 10)], (0, 10), length=10) invariant( @@ -1787,4 +1787,4 @@ def test_Coding_cds_end_equal_reference_length(): with pytest.raises(ValueError) as error: crossmap.coordinate_to_coding(10) - assert str(error.value) == 'Coordinate 10 is not within the bounds of the reference length 10.' + assert str(error.value) == 'Coordinate 10 is not within the bounds of the sequence length 10.' diff --git a/tests/test_locus.py b/tests/test_locus.py index 82c914f..9579ad4 100644 --- a/tests/test_locus.py +++ b/tests/test_locus.py @@ -83,7 +83,7 @@ def test_invalid_locus_coordinate(): def test_locus_negative_coordinate(): - """An offset may not convert to a coordinate before the reference start.""" + """An offset may not convert to a coordinate before the sequence start.""" with pytest.raises(ValueError) as error: Locus((0, 10)).to_coordinate(Point(position=0, offset=-1)) assert str(error.value) == 'Position 0 with offset -1 converts to negative coordinate -1.' diff --git a/tests/test_multi_locus.py b/tests/test_multi_locus.py index a217938..4f6cf87 100644 --- a/tests/test_multi_locus.py +++ b/tests/test_multi_locus.py @@ -57,7 +57,7 @@ def test_invalid_MultiLocus_initialization(): assert str(error.value) == 'Locus (30, 40) and locus (10, 20) are not in ascending order.' with pytest.raises(ValueError) as error: MultiLocus([(10, 12), (15, 25)], length=24) - assert str(error.value) == 'Location end 25 is inconsistent with reference length 24.' + assert str(error.value) == 'Location end 25 is inconsistent with sequence length 24.' with pytest.raises(ValueError) as error: MultiLocus([(10, 12), (15, 25)], length=0) assert str(error.value) == 'Value 0 is not positive.' @@ -96,7 +96,7 @@ def test_invalid_MultiLocus_initialization(): assert str(error.value) == 'Locus (10, 20) and locus (15, 25) are overlapping.' with pytest.raises(ValueError) as error: MultiLocus([(10, 12), (15, 25)], inverted=True, length=24) - assert str(error.value) == 'Location end 25 is inconsistent with reference length 24.' + assert str(error.value) == 'Location end 25 is inconsistent with sequence length 24.' def test_MultiLocus_invalid_coordinate(): @@ -719,8 +719,8 @@ def test_downstream_invalid_offset_inverted(): assert str(error.value) == 'Offset 6 exceeds downstream region.' -def test_MultiLocus_location_end_equal_reference_length(): - """Half-open location end may equal reference length.""" +def test_MultiLocus_location_end_equal_sequence_length(): + """Half-open location end may equal sequence length.""" multi_locus = MultiLocus([(0, 10)], length=10) invariant( @@ -732,4 +732,4 @@ def test_MultiLocus_location_end_equal_reference_length(): with pytest.raises(ValueError) as error: multi_locus.to_position(10) - assert str(error.value) == 'Coordinate 10 is not within the bounds of the reference length 10.' + assert str(error.value) == 'Coordinate 10 is not within the bounds of the sequence length 10.' From 57cf6da3e8cc4e59d782bbb27e054cb43097ee42 Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Thu, 10 Sep 2026 17:03:26 +0200 Subject: [PATCH 228/236] Fix the documentation examples and run them automatically. --- .readthedocs.yaml | 15 +++++ README.rst | 72 ++++++------------------ docs/api.rst | 2 +- docs/api/crossmap.rst | 2 + docs/api/locus.rst | 3 + docs/api/multi_locus.rst | 3 + docs/api/{dataclass.rst => point.rst} | 24 +++++--- docs/conf.py | 1 + docs/index.rst | 5 ++ docs/introduction.rst | 2 +- docs/library.rst | 79 ++++++++++++--------------- docs/requirements.txt | 4 +- mutalyzer_crossmapper/__init__.py | 2 +- mutalyzer_crossmapper/crossmapper.py | 53 ++++++++++-------- mutalyzer_crossmapper/location.py | 21 +++---- mutalyzer_crossmapper/locus.py | 8 +-- mutalyzer_crossmapper/multi_locus.py | 12 ++-- setup.cfg | 2 + tests/test_location.py | 36 ++++++------ 19 files changed, 174 insertions(+), 172 deletions(-) create mode 100644 .readthedocs.yaml rename docs/api/{dataclass.rst => point.rst} (58%) diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..7096477 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,15 @@ +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "3.12" + +sphinx: + configuration: docs/conf.py + +python: + install: + - requirements: docs/requirements.txt + - method: pip + path: . diff --git a/README.rst b/README.rst index ebd1ed6..2728059 100644 --- a/README.rst +++ b/README.rst @@ -36,7 +36,7 @@ numbering system to standard (0-based) coordinates and vice versa. - Support for genomic (``g.``, ``m.``, ``o.``) positions to standard coordinates and vice versa. - Support for noncoding (``n.``, ``r.``) positions to standard coordinates and vice versa. -- Support for coding (``c.``, ``r.``) positions to standard coordinates and vice versa. +- Support for coding (``c.``) positions to standard coordinates and vice versa. - Support for protein (``p.``) positions to standard coordinates and vice versa. - Basic classes that can be used for loci other than genes or transcripts. @@ -45,62 +45,24 @@ Please see ReadTheDocs_ for the latest documentation. Quick start ----------- -The ``Genomic`` class provides an interface to conversions between genomic -positions and coordinates. - -.. code:: python - - >>> from mutalyzer_crossmapper import Genomic - >>> crossmap = Genomic() - >>> crossmap.coordinate_to_genomic(0) - {'position': 1} - >>> crossmap.genomic_to_coordinate({'position': 1}) - 0 - -On top of the functionality provided by the ``Genomic`` class, the -``NonCoding`` class provides an interface to conversions between noncoding -positions and coordinates. - -.. code:: python - - >>> from mutalyzer_crossmapper import NonCoding - >>> exons = [(5, 8), (14, 20), (30, 35), (40, 44), (50, 52), (70, 72)] - >>> crossmap = NonCoding(exons) - >>> crossmap.coordinate_to_noncoding(35) - {'position': 14, 'offset': 1, 'region': ''} - >>> crossmap.noncoding_to_coordinate({'position': 14, 'offset': 1, 'region': ''}) - 35 - -Add the flag ``inverted=True`` to the constructor when the transcript resides -on the reverse complement strand. - -On top of the functionality provided by the ``NonCoding`` class, the ``Coding`` -class provides an interface to conversions between coding positions and -coordinates as well as conversions between protein positions and coordinates. +The ``Coding`` class converts zero-based sequence coordinates to HGVS coding +points and back. Exon locations and the CDS are given as zero-based half-open +intervals. .. code:: python >>> from mutalyzer_crossmapper import Coding - >>> cds = (32, 43) - >>> crossmap = Coding(exons, cds) - >>> crossmap.coordinate_to_coding(31) - {'position': 1, 'offset': 0, 'region': '-'} - >>> crossmap.coding_to_coordinate({'position':1, 'offset':0, 'region':'-'}) - 31 - -Again, the flag ``inverted=True`` can be used for transcripts that reside on -the reverse complement strand. - -Conversions between protein positions and coordinates are done as follows. - -.. code:: python - - >>> crossmap.coordinate_to_protein(41) - {'position': 2, 'position_in_codon': 2, 'offset': 0, 'region': ''} - >>> crossmap.protein_to_coordinate({'position': 2, 'position_in_codon': 2, 'offset': 0, 'region': ''}) - 41 - - - -.. _numbering: http://varnomen.hgvs.org/bg-material/numbering/ + >>> exons = [(5, 8), (11, 14)] + >>> crossmap = Coding(exons, cds=(6, 12)) + >>> point = crossmap.coordinate_to_coding(11) + >>> point + CodingPoint(position=3, offset=0, region='') + >>> crossmap.coding_to_coordinate(point) + 11 + +See the |library| for other numbering systems, intronic offsets, and +reverse complement transcripts. + +.. _numbering: https://hgvs-nomenclature.org/stable/background/numbering/ .. _ReadTheDocs: https://mutalyzer-crossmapper.readthedocs.io +.. |library| replace:: `library `__ diff --git a/docs/api.rst b/docs/api.rst index 1f7c54e..9ea1e49 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -10,4 +10,4 @@ API documentation api/location api/locus api/multi_locus - api/dataclass + api/point diff --git a/docs/api/crossmap.rst b/docs/api/crossmap.rst index 5455c89..1b75735 100644 --- a/docs/api/crossmap.rst +++ b/docs/api/crossmap.rst @@ -1,6 +1,8 @@ Crossmapper =========== +.. automodule:: mutalyzer_crossmapper.crossmapper + .. autoclass:: mutalyzer_crossmapper.crossmapper.Genomic :members: diff --git a/docs/api/locus.rst b/docs/api/locus.rst index 93aadff..ff76384 100644 --- a/docs/api/locus.rst +++ b/docs/api/locus.rst @@ -1,5 +1,8 @@ Locus ===== +See :class:`~mutalyzer_crossmapper.locus.Point` for the point fields. + .. automodule:: mutalyzer_crossmapper.locus :members: + :exclude-members: Point diff --git a/docs/api/multi_locus.rst b/docs/api/multi_locus.rst index 53d971e..58460a4 100644 --- a/docs/api/multi_locus.rst +++ b/docs/api/multi_locus.rst @@ -1,5 +1,8 @@ MultiLocus ========== +See :class:`~mutalyzer_crossmapper.multi_locus.Point` for the point fields. + .. automodule:: mutalyzer_crossmapper.multi_locus :members: + :exclude-members: Point diff --git a/docs/api/dataclass.rst b/docs/api/point.rst similarity index 58% rename from docs/api/dataclass.rst rename to docs/api/point.rst index 19988d9..f242845 100644 --- a/docs/api/dataclass.rst +++ b/docs/api/point.rst @@ -1,11 +1,8 @@ -Dataclass -========= +Point +===== -.. autoclass:: mutalyzer_crossmapper.locus.Point - :members: - -.. autoclass:: mutalyzer_crossmapper.multi_locus.Point - :members: +The following point classes encode the HGVS numbering systems and use a +one-based ``position`` field. .. autoclass:: mutalyzer_crossmapper.crossmapper.GenomicPoint :inherited-members: @@ -18,3 +15,16 @@ Dataclass .. autoclass:: mutalyzer_crossmapper.crossmapper.CodingPoint :inherited-members: :members: + +.. autoclass:: mutalyzer_crossmapper.crossmapper.ProteinPoint + :inherited-members: + :members: + +The internal point classes use zero-based positions relative to a locus or +concatenated loci. + +.. autoclass:: mutalyzer_crossmapper.locus.Point + :members: + +.. autoclass:: mutalyzer_crossmapper.multi_locus.Point + :members: diff --git a/docs/conf.py b/docs/conf.py index 3dbdd7d..1afc38b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,6 +9,7 @@ autoclass_content = 'both' extensions = [ 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', 'sphinx.ext.intersphinx' ] master_doc = 'index' diff --git a/docs/index.rst b/docs/index.rst index 7b6eb57..04ad073 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,9 @@ +.. Use an internal guide link in place of the README's external link. + .. include:: ../README.rst + :end-before: .. |library| replace:: + +.. |library| replace:: :doc:`library ` .. toctree:: :maxdepth: 2 diff --git a/docs/introduction.rst b/docs/introduction.rst index 5d5e301..9f78c35 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -22,4 +22,4 @@ interface that is able to convert from any HGVS numbering system to a conventional *coordinate* system and back. -.. _numbering: http://varnomen.hgvs.org/bg-material/numbering/ +.. _numbering: https://hgvs-nomenclature.org/stable/background/numbering/ diff --git a/docs/library.rst b/docs/library.rst index 4318611..09ab869 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -2,7 +2,7 @@ Library ======= The package provides conversion helpers between zero-based genomic -coordinates and HGVS-like point models for genomic, non-coding, coding, +coordinates and HGVS-like point models for genomic, noncoding, coding, and protein contexts. Coordinate And Location Conventions @@ -11,7 +11,8 @@ Coordinate And Location Conventions - Coordinates are zero-based integers. - Locations are provided as half-open intervals: ``(start, end)`` with ``start`` inclusive and ``end`` exclusive. -- HGVS-style positions exposed by public dataclasses are one-based. +- HGVS-style positions exposed by the point classes are one-based. +- Conversions between numbering systems should be done via a coordinate. The ``Genomic`` class @@ -23,17 +24,17 @@ The ``Genomic`` class provides an interface to conversions between genomic The ``GenomicPoint`` dataclass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Genomic positions follow the HGVS genomic coordinate system. -They are represented as 1-attribute dataclasses. Below is an example of ``g.1`` in HGVS. +Genomic positions follow the HGVS genomic coordinate system. A genomic +position is represented by a ``GenomicPoint`` instance with a single +``position`` attribute. Below is an example of ``g.1`` in HGVS. .. code-block:: python >>> from mutalyzer_crossmapper import GenomicPoint >>> GenomicPoint(position=1) + GenomicPoint(position=1) -Where: - -- **position**: an integer representing a nucleotide position (> 0) +See :doc:`api/point` for the fields. Genomic Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -60,26 +61,21 @@ The ``NonCoding`` class On top of the functionality provided by the ``Genomic`` class, the ``NonCoding`` class provides an interface to conversions between noncoding -(``n.``, ``r.``) positions and coordinates. Conversions between positioning -systems should be done via a coordinate. +(``n.``, ``r.``) positions and coordinates. -The ``NonCodingPoint`` Dataclass +The ``NonCodingPoint`` dataclass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -``NonCodingPoint`` fields: - -- ``position``: positive integer (1-based transcript position) -- ``offset``: integer intronic/outside offset -- ``region``: one of ``''``, ``'u'``, ``'d'`` - .. code-block:: python >>> from mutalyzer_crossmapper import NonCodingPoint >>> NonCodingPoint(position=14, offset=1, region='') NonCodingPoint(position=14, offset=1, region='') -Non-Coding Position Conversion -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +See :doc:`api/point` for the fields. + +Noncoding Position Conversion +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python @@ -101,7 +97,7 @@ Upstream and downstream positions are represented using ``region='u'`` and >>> crossmap.coordinate_to_noncoding(73) NonCodingPoint(position=22, offset=2, region='d') -For reverse-complement orientation, set ``inverted=True``: +For reverse complement orientation, set ``inverted=True``: .. code-block:: python @@ -112,27 +108,23 @@ For reverse-complement orientation, set ``inverted=True``: See :doc:`api/crossmap` for full API details. -The ``Coding`` Class +The ``Coding`` class -------------------- ``Coding`` extends ``NonCoding`` with coding DNA position logic -(``c.``, ``r.``), using exon locations and one CDS interval. +(``c.``), using exon locations and one CDS interval. -The ``CodingPoint`` Dataclass +The ``CodingPoint`` dataclass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -``CodingPoint`` fields: - -- ``position``: positive integer -- ``offset``: integer -- ``region``: one of ``''``, ``'u'``, ``'d'``, ``'-'``, ``'*'`` - .. code-block:: python >>> from mutalyzer_crossmapper import CodingPoint >>> CodingPoint(position=1, offset=3, region='*') CodingPoint(position=1, offset=3, region='*') +See :doc:`api/point` for the fields. + Coding Position Conversion ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -163,12 +155,10 @@ Protein Conversion ``Coding`` also exposes conversion to and from protein-position models. -The ``ProteinPoint`` Dataclass +The ``ProteinPoint`` dataclass ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -``ProteinPoint`` extends ``CodingPoint`` with: - -- ``position_in_codon``: one of ``1``, ``2``, ``3`` +``ProteinPoint`` extends ``CodingPoint`` with a position within the codon. .. code-block:: python @@ -176,6 +166,11 @@ The ``ProteinPoint`` Dataclass >>> ProteinPoint(position=1, position_in_codon=3, offset=0, region='') ProteinPoint(position=1, offset=0, region='', position_in_codon=3) +See :doc:`api/point` for the fields. + +Protein Position Conversion +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + .. code-block:: python >>> from mutalyzer_crossmapper import Coding @@ -219,20 +214,18 @@ Basic Classes These lower-level classes are used by ``NonCoding`` and ``Coding``. -The ``Point`` Dataclass +The ``Point`` dataclass ~~~~~~~~~~~~~~~~~~~~~~~ -``Point`` is the internal coordinate model used by ``Locus`` and -``MultiLocus``. - -- ``position``: zero-based position within a locus or concatenated loci -- ``offset``: relative offset -- ``region``: one of ``''``, ``'u'``, ``'d'`` (mainly for ``MultiLocus``) +``Locus`` and ``MultiLocus`` each have their own ``Point`` dataclass. +``locus.Point`` holds a position and an offset; ``multi_locus.Point`` adds a +region to distinguish upstream and downstream positions from positions +within or between the loci. -See :doc:`api/dataclass`. +See :doc:`api/point` for both. -The ``Locus`` Class +The ``Locus`` class ~~~~~~~~~~~~~~~~~~~ ``Locus`` maps one genomic interval to/from ``Point``. @@ -246,12 +239,12 @@ The ``Locus`` Class >>> locus.to_coordinate(Point(position=0, offset=-1)) 9 -Set ``inverted=True`` for reverse-complement orientation. +Set ``inverted=True`` for reverse complement orientation. See :doc:`api/locus` for full API details. -The ``MultiLocus`` Class +The ``MultiLocus`` class ~~~~~~~~~~~~~~~~~~~~~~~~ ``MultiLocus`` maps coordinates across multiple intervals to/from a unified diff --git a/docs/requirements.txt b/docs/requirements.txt index 7f5d7b8..3325317 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1 @@ -docutils==0.17.1 -sphinx-argparse -sphinx-autodoc-typehints +sphinx>=7.2 diff --git a/mutalyzer_crossmapper/__init__.py b/mutalyzer_crossmapper/__init__.py index 48fb268..00feb23 100644 --- a/mutalyzer_crossmapper/__init__.py +++ b/mutalyzer_crossmapper/__init__.py @@ -1,7 +1,7 @@ from importlib.metadata import metadata from .crossmapper import Coding, Genomic, NonCoding, GenomicPoint, NonCodingPoint, CodingPoint, ProteinPoint -from .location import _nearest_location +from .location import nearest_location from .locus import Locus from .multi_locus import MultiLocus, Point diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 9fb2a29..44b056d 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -17,7 +17,7 @@ _check_multi_locus, ) from .locus import _check_locus, _check_int, _check_non_negative_int, _check_positive_int -from .location import _nearest_location +from .location import nearest_location @dataclass(slots=True) class GenomicPoint: @@ -55,12 +55,12 @@ class Genomic(): """ def coordinate_to_genomic(self, coordinate: int, length: int | None = None) -> GenomicPoint: - """Convert a coordinate to a genomic point dataclass (g./m./o.). + """Convert a coordinate to a genomic point (g./m./o.). :arg coordinate: Zero-based coordinate, non-negative. :arg length: Length of the sequence, None if unknown. - :returns: Genomic point dataclass. + :returns: Genomic point. :raises ValueError: If the coordinate is not a non-negative integer, if the length is not a positive integer, or if the coordinate @@ -73,9 +73,9 @@ def coordinate_to_genomic(self, coordinate: int, length: int | None = None) -> G return GenomicPoint(coordinate + 1) def genomic_to_coordinate(self, point: GenomicPoint) -> int: - """Convert a genomic point dataclass (g./m./o.) to a coordinate. + """Convert a genomic point (g./m./o.) to a coordinate. - :arg point: Genomic point dataclass. + :arg point: Genomic point. :returns: Coordinate. """ @@ -192,11 +192,11 @@ def __init__( self._noncoding = MultiLocus(locations, inverted=inverted, length=length) def coordinate_to_noncoding(self, coordinate: int) -> NonCodingPoint: - """Convert a coordinate to a noncoding point dataclass (n./r.). + """Convert a coordinate to a noncoding point (n./r.). :arg coordinate: Zero-based coordinate, non-negative. - :returns: Noncoding point dataclass. + :returns: Noncoding point. :raises ValueError: If the coordinate is not a non-negative integer, or goes outside of the sequence length. @@ -209,9 +209,9 @@ def coordinate_to_noncoding(self, coordinate: int) -> NonCodingPoint: ) def noncoding_to_coordinate(self, point: NonCodingPoint) -> int: - """Convert a noncoding point dataclass (n./r.) to a coordinate. + """Convert a noncoding point (n./r.) to a coordinate. - :arg point: Noncoding point dataclass. + :arg point: Noncoding point. :returns: Coordinate. @@ -320,6 +320,13 @@ class adds the coding and the protein numbering systems. The positions >>> crossmap.coordinate_to_protein(11) ProteinPoint(position=1, offset=0, region='', position_in_codon=3) + + The noncoding conversions are inherited, so the same transcript can also + be addressed in the noncoding (n./r.) numbering, which counts over the + exons instead of from the start of the CDS. + + >>> crossmap.coordinate_to_noncoding(12) + NonCodingPoint(position=5, offset=0, region='') """ def __init__( @@ -402,7 +409,7 @@ def _check_cds( f'exon {locations[-1]}.' ) - index = _nearest_location(locations, coordinate) + index = nearest_location(locations, coordinate) start, end = locations[index] if start <= coordinate <= end: continue @@ -455,14 +462,14 @@ def _validate_point(self, position: int, region: str) -> None: def _coordinate_to_coding(self, coordinate: int) -> CodingPoint: - """Convert a coordinate to a coding point dataclass (c.). + """Convert a coordinate to a coding point (c.). A coordinate outside the transcript is given the ``'u'`` or ``'d'`` region, leaving the degenerate point to ``coordinate_to_coding``. :arg coordinate: Zero-based coordinate, non-negative. - :returns: Coding point dataclass (c.). + :returns: Coding point (c.). """ noncoding_point = self._noncoding.to_position(coordinate) @@ -492,7 +499,7 @@ def _coordinate_to_coding(self, coordinate: int) -> CodingPoint: return CodingPoint(position=position, offset=offset, region=region) def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> CodingPoint: - """Convert a coordinate to a coding point dataclass (c.). + """Convert a coordinate to a coding point (c.). A coordinate outside the transcript is given the ``'u'`` or ``'d'`` region. With ``degenerate`` set it is expressed in the ``'-'`` or @@ -514,9 +521,9 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> Cod CodingPoint(position=3, offset=0, region='*') :arg coordinate: Zero-based coordinate, non-negative. - :arg degenerate: Return a degenerate coding point dataclass. + :arg degenerate: Return a degenerate coding point. - :returns: Coding point dataclass (c.). + :returns: Coding point (c.). :raises ValueError: If the coordinate is not a non-negative integer, or goes outside of the sequence length. @@ -542,12 +549,12 @@ def coordinate_to_coding(self, coordinate: int, degenerate: bool = False) -> Cod return point def _coding_to_coordinate(self, point: CodingPoint) -> int: - """Convert a coding point dataclass (c.) to a coordinate. + """Convert a coding point (c.) to a coordinate. A degenerate point is rejected rather than corrected, so the position must stay within the region it names. - :arg point: Coding point dataclass (c.). + :arg point: Coding point (c.). :returns: Coordinate. """ @@ -586,13 +593,13 @@ def _coding_to_coordinate(self, point: CodingPoint) -> int: raise def coding_to_coordinate(self, point: CodingPoint) -> int: - """Convert a coding point dataclass (c.) to a coordinate. + """Convert a coding point (c.) to a coordinate. A degenerate point, one with offset zero whose position counts on past the end of the transcript in the ``'-'`` or ``'*'`` region, is silently corrected. - :arg point: Coding point dataclass (c.). + :arg point: Coding point (c.). :returns: Coordinate. @@ -635,7 +642,7 @@ def coding_to_coordinate(self, point: CodingPoint) -> int: ) from error def coordinate_to_protein(self, coordinate: int) -> ProteinPoint: - """Convert a coordinate to a protein point dataclass (p.). + """Convert a coordinate to a protein point (p.). The position is that of the codon, with ``position_in_codon`` selecting one of its three nucleotides. A codon may be split by an @@ -659,7 +666,7 @@ def coordinate_to_protein(self, coordinate: int) -> ProteinPoint: :arg coordinate: Zero-based coordinate, non-negative. - :returns: Protein point dataclass (p.). + :returns: Protein point (p.). :raises ValueError: If the coordinate is not a non-negative integer, or goes outside of the sequence length. @@ -682,7 +689,7 @@ def coordinate_to_protein(self, coordinate: int) -> ProteinPoint: ) def protein_to_coordinate(self, point: ProteinPoint) -> int: - """Convert a protein point dataclass (p.) to a coordinate. + """Convert a protein point (p.) to a coordinate. A degenerate point is silently corrected, as in ``coding_to_coordinate``. @@ -692,7 +699,7 @@ def protein_to_coordinate(self, point: ProteinPoint) -> int: the intermediate coding nucleotide position rather than the supplied protein position. - :arg point: Protein point dataclass (p.). + :arg point: Protein point (p.). :returns: Coordinate. diff --git a/mutalyzer_crossmapper/location.py b/mutalyzer_crossmapper/location.py index 30ffeaf..37593b4 100644 --- a/mutalyzer_crossmapper/location.py +++ b/mutalyzer_crossmapper/location.py @@ -2,12 +2,12 @@ def _nearest_boundary(lb: int, rb: int, c: int, p: int) -> int: """Find the boundary nearest to `c`. In case of a draw, the parameter `p` decides which one is chosen. - :arg int lb: Left boundary. - :arg int rb: Right boundary. - :arg int c: Coordinate (`lb` <= `c` <= `rb`)). - :arg int p: Preference in case of a draw: 0: left, 1: right. + :arg lb: Left boundary. + :arg rb: Right boundary. + :arg c: Coordinate (`lb` <= `c` <= `rb`)). + :arg p: Preference in case of a draw: 0: left, 1: right. - :returns int: Nearest boundary: 0: left, 1: right. + :returns: Nearest boundary: 0: left, 1: right. """ dl = c - lb + 1 dr = rb - c @@ -19,15 +19,16 @@ def _nearest_boundary(lb: int, rb: int, c: int, p: int) -> int: return p -def _nearest_location(ls: list[tuple[int, int]], c: int, p: int = 0) -> int: +def nearest_location(ls: list[tuple[int, int]], c: int, p: int = 0) -> int: """Find the location nearest to `c`. In case of a draw, the parameter `p` decides which index is chosen. - :arg list ls: List of locations. - :arg int c: Coordinate. - :arg int p: Preference in case of a draw: 0: left, 1: right. + :arg ls: Non-empty list of half-open intervals, sorted ascending and + non-overlapping. + :arg c: Coordinate. + :arg p: Preference in case of a draw: 0: left, 1: right. - :returns int: Nearest location. + :returns: Zero-based index of the nearest location. """ rb = len(ls) - 1 lb = 0 diff --git a/mutalyzer_crossmapper/locus.py b/mutalyzer_crossmapper/locus.py index 3eecde5..10aada0 100644 --- a/mutalyzer_crossmapper/locus.py +++ b/mutalyzer_crossmapper/locus.py @@ -129,14 +129,14 @@ def _validate_point(self, position: int, offset: int) -> None: raise IndexError(f'Position {position} exceeds locus length.') def to_position(self, coordinate: int) -> Point: - """Convert a coordinate to a locus point dataclass. + """Convert a coordinate to a locus point. Coordinates outside the locus are converted to a boundary position with a non-zero offset. :arg coordinate: Zero-based coordinate, non-negative. - :returns: Locus point dataclass. + :returns: Locus point. :raises ValueError: If the coordinate is not a non-negative integer. """ @@ -156,9 +156,9 @@ def to_position(self, coordinate: int) -> Point: return Point(position=coordinate - self.boundary[0], offset=0) def to_coordinate(self, point: Point) -> int: - """Convert a locus point dataclass to a coordinate. + """Convert a locus point to a coordinate. - :arg point: Locus point dataclass, its position relative to this locus. + :arg point: Locus point, its position relative to this locus. :returns: Coordinate. diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index cd9fdb0..1b5cae9 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -10,7 +10,7 @@ from itertools import accumulate from dataclasses import dataclass -from .location import _nearest_location +from .location import nearest_location from .locus import Locus, _check_locus, _check_non_negative_int, _check_positive_int from .locus import Point as LocusPoint @@ -284,7 +284,7 @@ def _outside(self, coordinate: int) -> int: return 0 def to_position(self, coordinate: int) -> Point: - """Convert a coordinate to a multi locus point dataclass. + """Convert a coordinate to a multi locus point. A coordinate in a gap between two loci is always given the nearest boundary form, and one outside the loci altogether the ``'u'`` or @@ -292,14 +292,14 @@ def to_position(self, coordinate: int) -> Point: :arg coordinate: Zero-based coordinate, non-negative. - :returns: Multi locus point dataclass. + :returns: Multi locus point. :raises ValueError: If the coordinate is not a non-negative integer, or lies beyond the sequence. """ _check_non_negative_int(coordinate) self._validate_coord(coordinate) - index = _nearest_location(self._locations, coordinate, self._inverted) + index = nearest_location(self._locations, coordinate, self._inverted) outside = self._orientation * self._outside(coordinate) region = 'u' if outside < 0 else 'd' if outside > 0 else '' point = self._loci[index].to_position(coordinate) @@ -311,13 +311,13 @@ def to_position(self, coordinate: int) -> Point: ) def to_coordinate(self, point: Point) -> int: - """Convert a multi locus point dataclass to a coordinate. + """Convert a multi locus point to a coordinate. A coordinate in a gap between two loci can be described from either flanking locus and both forms are accepted, so converting the result back with ``to_position`` does not always give the original point. - :arg point: Multi locus point dataclass, its position relative to the + :arg point: Multi locus point, its position relative to the concatenated loci. :returns: Coordinate. diff --git a/setup.cfg b/setup.cfg index 0b187ae..d0c8065 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,6 +25,8 @@ tests = pytest>=5.4.3 [tool:pytest] +addopts = --doctest-modules --doctest-glob=*.rst +testpaths = mutalyzer_crossmapper tests README.rst docs/library.rst [coverage:run] source = mutalyzer_crossmapper diff --git a/tests/test_location.py b/tests/test_location.py index f912463..0841ed3 100644 --- a/tests/test_location.py +++ b/tests/test_location.py @@ -1,4 +1,4 @@ -from mutalyzer_crossmapper import _nearest_location +from mutalyzer_crossmapper import nearest_location from mutalyzer_crossmapper.location import _nearest_boundary @@ -20,36 +20,36 @@ def test_nearest_location(): """Index of the nearest location.""" locations = [(10, 20), (30, 40), (50, 60)] - assert _nearest_location(locations, 8) == 0 - assert _nearest_location(locations, 15) == 0 - assert _nearest_location(locations, 22) == 0 + assert nearest_location(locations, 8) == 0 + assert nearest_location(locations, 15) == 0 + assert nearest_location(locations, 22) == 0 - assert _nearest_location(locations, 28) == 1 - assert _nearest_location(locations, 35) == 1 - assert _nearest_location(locations, 42) == 1 + assert nearest_location(locations, 28) == 1 + assert nearest_location(locations, 35) == 1 + assert nearest_location(locations, 42) == 1 - assert _nearest_location(locations, 48) == 2 - assert _nearest_location(locations, 55) == 2 - assert _nearest_location(locations, 62) == 2 + assert nearest_location(locations, 48) == 2 + assert nearest_location(locations, 55) == 2 + assert nearest_location(locations, 62) == 2 def test_nearest_location_even(): """Index of the nearest location, preference is irrelevant.""" - assert _nearest_location([(3, 6), (8, 13)], 6, 0) == 0 - assert _nearest_location([(3, 6), (8, 13)], 6, 1) == 0 - assert _nearest_location([(3, 6), (8, 13)], 7, 0) == 1 - assert _nearest_location([(3, 6), (8, 13)], 7, 1) == 1 + assert nearest_location([(3, 6), (8, 13)], 6, 0) == 0 + assert nearest_location([(3, 6), (8, 13)], 6, 1) == 0 + assert nearest_location([(3, 6), (8, 13)], 7, 0) == 1 + assert nearest_location([(3, 6), (8, 13)], 7, 1) == 1 def test_nearest_location_odd(): """Index of the nearest location, preference is relevant.""" - assert _nearest_location([(3, 6), (9, 13)], 7) == 0 - assert _nearest_location([(3, 6), (9, 13)], 7, 1) == 1 + assert nearest_location([(3, 6), (9, 13)], 7) == 0 + assert nearest_location([(3, 6), (9, 13)], 7, 1) == 1 def test_nearest_location_adjacent(): """Adjacent locations have no overlap.""" locations = [(1, 3), (3, 5)] - assert _nearest_location(locations, 2) == 0 - assert _nearest_location(locations, 3) == 1 + assert nearest_location(locations, 2) == 0 + assert nearest_location(locations, 3) == 1 From 98846c43b0f678b066e8eacc35a548f7261460fc Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Thu, 10 Sep 2026 21:06:00 +0200 Subject: [PATCH 229/236] Let each class in the hierarchy check only what it owns. --- mutalyzer_crossmapper/crossmapper.py | 2 -- mutalyzer_crossmapper/multi_locus.py | 20 +++++++++----------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 44b056d..a5d3dc7 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -14,7 +14,6 @@ Point, _check_coordinate_within_length, _check_location_end_within_length, - _check_multi_locus, ) from .locus import _check_locus, _check_int, _check_non_negative_int, _check_positive_int from .location import nearest_location @@ -187,7 +186,6 @@ def __init__( is not a boolean, or if the length is not a positive integer large enough to contain the exons. """ - _check_multi_locus(locations, length) self._inverted = inverted self._noncoding = MultiLocus(locations, inverted=inverted, length=length) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 1b5cae9..a1b0777 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -11,7 +11,7 @@ from dataclasses import dataclass from .location import nearest_location -from .locus import Locus, _check_locus, _check_non_negative_int, _check_positive_int +from .locus import Locus, _check_non_negative_int, _check_positive_int from .locus import Point as LocusPoint @@ -54,14 +54,8 @@ def _check_location_end_within_length(value: int, length: int) -> None: ) -def _check_multi_locus(locations: list[tuple[int, int]], length: int | None = None) -> None: - """Check if the locations list is valid.""" - if not locations or not isinstance(locations, list): - raise ValueError('Locations must be a non-empty list of tuples.') - - for locus in locations: - _check_locus(locus) - +def _check_locations(locations: list[tuple[int, int]], length: int | None = None) -> None: + """Check ordering, overlap and sequence bounds of the locations.""" for l1, l2 in zip(locations, locations[1:]): if l2[0] < l1[0]: raise ValueError(f'Locus {l1} and locus {l2} are not in ascending order.') @@ -149,14 +143,18 @@ def __init__( is not a boolean, or if the length is not a positive integer the last location fits in. """ - _check_multi_locus(locations, length) + if not isinstance(locations, list) or not locations: + raise ValueError('Locations must be a non-empty list of tuples.') if not isinstance(inverted, bool): raise ValueError(f'Value {inverted} is not a boolean.') + + self._loci = [Locus(location, inverted) for location in locations] + _check_locations(locations, length) + self._locations = locations self._inverted = inverted self._length = length - self._loci = [Locus(location, inverted) for location in locations] self._orientation = -1 if inverted else 1 self._offsets = _offsets(locations, self._orientation) # one-based length of the MultiLocus From 165c7bf6ac9debf2be5cb9cb633829880f8483ee Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Thu, 10 Sep 2026 21:56:54 +0200 Subject: [PATCH 230/236] Stop recomputing what Locus already does. --- mutalyzer_crossmapper/multi_locus.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index a1b0777..84a3b79 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -331,18 +331,9 @@ def to_coordinate(self, point: Point) -> int: ) self._validate_point(index, point.position, point.offset, point.region) - if point.region == 'u': - if self._inverted: - return self._locations[-1][1] - point.offset - 1 - return self._locations[0][0] + point.offset - if point.region == 'd': - if self._inverted: - return self._locations[0][0] - point.offset - return self._locations[-1][1] + point.offset - 1 - try: return self._loci[self._direction(index)].to_coordinate( - Point( + LocusPoint( position=point.position - self._offsets[index], offset=point.offset, ) From a0ffafcadf276b6a029590d9f9997eb1b59ce243 Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Thu, 10 Sep 2026 22:22:49 +0200 Subject: [PATCH 231/236] Simplify multi locus point validation. --- mutalyzer_crossmapper/multi_locus.py | 94 ++++++++++------------------ 1 file changed, 32 insertions(+), 62 deletions(-) diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index 84a3b79..a6ce730 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -192,75 +192,45 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - raise ValueError(f'Position {position} is not at upstream boundary.') if offset >= 0: raise ValueError(f'Offset {offset} at upstream region should be negative.') - if self._inverted: - if ( - self._length is not None - and -offset - >= self._length - self._loci[self._direction(0)].boundary[1] - ): - raise ValueError(f'Offset {offset} exceeds upstream region.') - else: - if -offset > self._loci[self._direction(0)].boundary[0]: - raise ValueError(f'Offset {offset} exceeds upstream region.') if region == 'd': if position != self._end - 1: raise ValueError(f'Position {position} is not at downstream boundary.') if offset <= 0: raise ValueError(f'Offset {offset} at downstream region should be positive.') - if not self._inverted: - if ( - self._length is not None - and offset - >= self._length - self._loci[self._direction(-1)].boundary[1] - ): - raise ValueError(f'Offset {offset} exceeds downstream region.') + + if region in ('u', 'd'): + if self._orientation * offset < 0: + max_offset = self._loci[0].boundary[0] + elif self._length is not None: + max_offset = self._length - self._loci[-1].boundary[1] - 1 else: - if ( - offset - > self._loci[self._direction(len(self._locations) - 1)].boundary[0] - ): - raise ValueError(f'Offset {offset} exceeds downstream region.') - - if region == '': - if offset < 0: - if index == 0 and position == 0: - raise ValueError(f'Offset {offset} at the first locus should be in the upstream region.') - if self._inverted: - if ( - self._direction(index) != len(self._loci) - 1 - and -offset - >= self._loci[self._direction(index - 1)].boundary[0] - - self._loci[self._direction(index)].boundary[1] - ): - raise IndexError(f'Offset {offset} exceeds intron length.') - else: - if ( - self._direction(index) != 0 - and -offset - >= self._loci[self._direction(index)].boundary[0] - - self._loci[self._direction(index - 1)].boundary[1] - ): - raise IndexError(f'Offset {offset} exceeds intron length.') - if offset > 0: - if index == len(self._loci) - 1 and position == self._end - 1: - raise ValueError(f'Offset {offset} at the last locus should be in the downstream region.') - if self._inverted: - if ( - self._direction(index) != 0 - and offset - >= self._loci[self._direction(index)].boundary[0] - - self._loci[self._direction(index + 1)].boundary[1] - ): - raise IndexError(f'Offset {offset} exceeds intron length.') - else: - if ( - self._direction(index) != len(self._loci) - 1 - and offset - >= self._loci[self._direction(index + 1)].boundary[0] - - self._loci[self._direction(index)].boundary[1] - ): - raise IndexError(f'Offset {offset} exceeds intron length.') + return + + if abs(offset) > max_offset: + side = 'upstream' if region == 'u' else 'downstream' + raise ValueError(f'Offset {offset} exceeds {side} region.') + + if region == '' and offset != 0: + if offset < 0 and index == 0 and position == 0: + raise ValueError( + f'Offset {offset} at the first locus should be in the upstream region.' + ) + if offset > 0 and index == len(self._loci) - 1 and position == self._end - 1: + raise ValueError( + f'Offset {offset} at the last locus should be in the downstream region.' + ) + + neighbor_index = index + (-1 if offset < 0 else 1) + if 0 <= neighbor_index < len(self._loci): + left_index = min(self._direction(index), self._direction(neighbor_index)) + gap_length = ( + self._loci[left_index + 1].boundary[0] + - self._loci[left_index].boundary[1] + - 1 + ) + if abs(offset) > gap_length: + raise IndexError(f'Offset {offset} exceeds intron length.') def _direction(self, index: int) -> int: """Convert a locus index between position and coordinate order.""" From ca6df6a09e2faeda577b7021f569f7532916f1a1 Mon Sep 17 00:00:00 2001 From: Mihai Lefter Date: Fri, 11 Sep 2026 09:51:12 +0200 Subject: [PATCH 232/236] Ask the MultiLocus for its length instead of deriving it. --- mutalyzer_crossmapper/crossmapper.py | 27 +++++++-------------------- mutalyzer_crossmapper/multi_locus.py | 18 +++++++++--------- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index a5d3dc7..6aacf68 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -356,27 +356,14 @@ def __init__( cds_start = self._noncoding.to_position(cds[0]) cds_end = self._noncoding.to_position(cds[1] - 1) - exon_start = self._noncoding.to_position(locations[0][0]) - exon_end = self._noncoding.to_position(locations[-1][1] - 1) - if self._inverted: - self._coding = ( - cds_end.position + cds_end.offset, - cds_start.position + cds_start.offset + 1 - ) - self._exons = ( - exon_end.position + exon_end.offset, - exon_start.position + exon_start.offset + 1 - ) - else: - self._coding = ( - cds_start.position + cds_start.offset, - cds_end.position + cds_end.offset + 1 - ) - self._exons = ( - exon_start.position + exon_start.offset, - exon_end.position + exon_end.offset + 1 - ) + cds_start, cds_end = cds_end, cds_start + + self._coding = ( + cds_start.position + cds_start.offset, + cds_end.position + cds_end.offset + 1, + ) + self._exons = (0, self._noncoding._position_length) def _check_cds( self, diff --git a/mutalyzer_crossmapper/multi_locus.py b/mutalyzer_crossmapper/multi_locus.py index a6ce730..e896953 100644 --- a/mutalyzer_crossmapper/multi_locus.py +++ b/mutalyzer_crossmapper/multi_locus.py @@ -153,20 +153,20 @@ def __init__( self._locations = locations self._inverted = inverted - self._length = length + self._sequence_length = length self._orientation = -1 if inverted else 1 self._offsets = _offsets(locations, self._orientation) - # one-based length of the MultiLocus - self._end = sum(end - start for start, end in locations) + # number of positions covered by the loci, gaps excluded + self._position_length = sum(end - start for start, end in locations) def _validate_coord(self, coordinate: int) -> None: """Check that the coordinate is within the sequence. Without a sequence length there is no upper bound to check against. """ - if self._length is not None: - _check_coordinate_within_length(coordinate, self._length) + if self._sequence_length is not None: + _check_coordinate_within_length(coordinate, self._sequence_length) def _validate_point(self, index: int, position: int, offset: int, region: str) -> None: """Check that a point is described from the right side of the loci. @@ -194,7 +194,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - raise ValueError(f'Offset {offset} at upstream region should be negative.') if region == 'd': - if position != self._end - 1: + if position != self._position_length - 1: raise ValueError(f'Position {position} is not at downstream boundary.') if offset <= 0: raise ValueError(f'Offset {offset} at downstream region should be positive.') @@ -202,8 +202,8 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - if region in ('u', 'd'): if self._orientation * offset < 0: max_offset = self._loci[0].boundary[0] - elif self._length is not None: - max_offset = self._length - self._loci[-1].boundary[1] - 1 + elif self._sequence_length is not None: + max_offset = self._sequence_length - self._loci[-1].boundary[1] - 1 else: return @@ -216,7 +216,7 @@ def _validate_point(self, index: int, position: int, offset: int, region: str) - raise ValueError( f'Offset {offset} at the first locus should be in the upstream region.' ) - if offset > 0 and index == len(self._loci) - 1 and position == self._end - 1: + if offset > 0 and index == len(self._loci) - 1 and position == self._position_length - 1: raise ValueError( f'Offset {offset} at the last locus should be in the downstream region.' ) From 36d24e44be07e08b0eb5c438ed8f9d533df1201e Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 15 Sep 2026 12:03:18 +0200 Subject: [PATCH 233/236] Specify allowed position values in upstream. --- mutalyzer_crossmapper/crossmapper.py | 10 +++- tests/test_crossmapper.py | 73 +++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index 6aacf68..d2b7b9f 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -364,6 +364,7 @@ def __init__( cds_end.position + cds_end.offset + 1, ) self._exons = (0, self._noncoding._position_length) + print(self._coding, self._exons) def _check_cds( self, @@ -423,7 +424,14 @@ def _validate_point(self, position: int, region: str) -> None: ``'d'`` region. """ if region == 'u': - if position not in (1, self._coding[0]): + allowed: tuple[int, ...] + if self._exons[0] == self._coding[0]: + # Without a 5' UTR the anchor is the first coding position, + # and the degenerate correction anchors at position 1. + allowed = (1,) + else: + allowed = (self._coding[0],) + if position not in allowed: raise ValueError(f'Position {position} is not in upstream boundary.') if region == '-': if position not in range(1, self._coding[0] + 1): diff --git a/tests/test_crossmapper.py b/tests/test_crossmapper.py index 24f3d4b..61ce956 100644 --- a/tests/test_crossmapper.py +++ b/tests/test_crossmapper.py @@ -913,6 +913,34 @@ def test_Coding_no_utr5(): CodingPoint(position=1, offset=0, region=''), ) + degenerate_equal( + crossmap.coding_to_coordinate, + 9, + [ + CodingPoint(position=1, offset=-1, region='u'), + CodingPoint(position=1, offset=0, region='-'), + ], + ) + + +def test_Coding_no_utr5_unequal_utr3(): + """Without a 5' UTR the downstream anchor is the last coding position.""" + crossmap = Coding([(10, 20)], (10, 15)) + # Direct transition from CDS to downstream. + invariant( + crossmap.coordinate_to_coding, + 9, + crossmap.coding_to_coordinate, + CodingPoint(position=1, offset=-1, region='u'), + ) + invariant( + crossmap.coordinate_to_coding, + 10, + crossmap.coding_to_coordinate, + CodingPoint(position=1, offset=0, region=''), + ) + + def test_Coding_no_utr5_inverted(): """A 5' UTR may be missing.""" @@ -931,6 +959,41 @@ def test_Coding_no_utr5_inverted(): crossmap.coding_to_coordinate, CodingPoint(position=1, offset=0, region=''), ) + degenerate_equal( + crossmap.coding_to_coordinate, + 20, + [ + CodingPoint(position=1, offset=-1, region='u'), + CodingPoint(position=1, offset=0, region='-'), + ], + ) + + +def test_Coding_no_utr5_inverted_unequal_utr3(): + """A 5' UTR may be missing and the 3' UTR is unequal.""" + crossmap = Coding([(10, 20)], (15, 20), inverted=True) + + # Direct transition from upstream to CDS. + invariant( + crossmap.coordinate_to_coding, + 20, + crossmap.coding_to_coordinate, + CodingPoint(position=1, offset=-1, region='u'), + ) + invariant( + crossmap.coordinate_to_coding, + 19, + crossmap.coding_to_coordinate, + CodingPoint(position=1, offset=0, region=''), + ) + degenerate_equal( + crossmap.coding_to_coordinate, + 20, + [ + CodingPoint(position=1, offset=-1, region='u'), + CodingPoint(position=1, offset=0, region='-'), + ], + ) def test_Coding_no_utr3(): @@ -1406,6 +1469,9 @@ def test_Coding_invalid_position(): with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=12, offset=-1, region='u')) assert str(error.value) == 'Position 12 is not in upstream boundary.' + with pytest.raises(ValueError) as error: + crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-1, region='u')) + assert str(error.value) == 'Position 1 is not in upstream boundary.' with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=13, offset=1, region='-')) assert str(error.value) == 'Position 13 exceeds - region.' @@ -1445,6 +1511,9 @@ def test_Coding_inverted_invalid_position_inverted(): with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=12, offset=-1, region='u')) assert str(error.value) == 'Position 12 is not in upstream boundary.' + with pytest.raises(ValueError) as error: + crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-2, region='u')) + assert str(error.value) == 'Position 1 is not in upstream boundary.' with pytest.raises(ValueError) as error: crossmap.coding_to_coordinate(CodingPoint(position=13, offset=1, region='-')) assert str(error.value) == 'Position 13 exceeds - region.' @@ -1479,7 +1548,7 @@ def test_Coding_invalid_offset(): crossmap = Coding(_exons, _cds, length=75) with pytest.raises(ValueError) as error: - crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-6, region='u')) + crossmap.coding_to_coordinate(CodingPoint(position=11, offset=-6, region='u')) assert str(error.value) == 'Offset -6 exceeds upstream region.' # A degenerate position beyond the sequence is reported as written. with pytest.raises(ValueError) as error: @@ -1522,7 +1591,7 @@ def test_Coding_invalid_offset_inverted(): crossmap = Coding(_exons, _cds, inverted=True, length=75) with pytest.raises(ValueError) as error: - crossmap.coding_to_coordinate(CodingPoint(position=1, offset=-6, region='u')) + crossmap.coding_to_coordinate(CodingPoint(position=5, offset=-6, region='u')) assert str(error.value) == 'Offset -6 exceeds upstream region.' # A degenerate position beyond the sequence is reported as written. with pytest.raises(ValueError) as error: From 192f829dccdcad50cc35e120bc49e4384fd32060 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 15 Sep 2026 12:12:28 +0200 Subject: [PATCH 234/236] Fix typing error. --- mutalyzer_crossmapper/crossmapper.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index d2b7b9f..e4a17e2 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -443,7 +443,6 @@ def _validate_point(self, position: int, region: str) -> None: if position not in range(1, self._exons[1] - self._coding[1] + 1): raise ValueError(f'Position {position} exceeds * region.') if region == 'd': - allowed: tuple[int, ...] if self._exons[1] == self._coding[1]: # Without a 3' UTR the anchor is the last coding position, # and the degenerate correction anchors at position 1. From d7f6782eddaf636630f244f70fd15d8c89a08069 Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Tue, 15 Sep 2026 12:18:51 +0200 Subject: [PATCH 235/236] Cleanup. --- mutalyzer_crossmapper/crossmapper.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index e4a17e2..d1fd362 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -364,7 +364,6 @@ def __init__( cds_end.position + cds_end.offset + 1, ) self._exons = (0, self._noncoding._position_length) - print(self._coding, self._exons) def _check_cds( self, From b57871abff5e7f61f3e9b65ad0f51600d9e0f1bd Mon Sep 17 00:00:00 2001 From: Xiaoyun Liu Date: Wed, 16 Sep 2026 09:55:08 +0200 Subject: [PATCH 236/236] Assign constant value in downstream regardless of missing 3'UTR. --- mutalyzer_crossmapper/crossmapper.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mutalyzer_crossmapper/crossmapper.py b/mutalyzer_crossmapper/crossmapper.py index d1fd362..f25c626 100644 --- a/mutalyzer_crossmapper/crossmapper.py +++ b/mutalyzer_crossmapper/crossmapper.py @@ -554,15 +554,11 @@ def _coding_to_coordinate(self, point: CodingPoint) -> int: self._validate_point(position, region) - # For missing 3' UTR or 5' UTR if region in ('u', 'd'): if region == 'u': position = 1 if region == 'd': - if self._coding[1] == self._exons[1]: - position = self._coding[1] - else: - position = position + self._coding[1] + position = self._exons[1] return self._noncoding.to_coordinate( Point(position=position - 1, region=point.region, offset=point.offset) )