Repository: Edinburgh-Genome-Foundry/DnaChisel

Issue:
Issue with AvoidPattern (and potentially other builtin_specifications)
It looks as though AvoidPattern doesn't preserve its strand information when being localized if the original strand information is 'from_location'

https://github.com/Edinburgh-Genome-Foundry/DnaChisel/blob/db0b606ec993a509de9f3bb4d8acde9baadeef99/dnachisel/builtin_specifications/AvoidPattern.py#L85

Example illustrating this:
```
ipdb> cst
    # AvoidPattern[4-1624(-)](pattern:AACAAAT)

ipdb> location
    # 9-10

ipdb> new_cst = cst.localized(location, problem=self)
ipdb> new_cst
    # AvoidPattern[4-16](pattern:AACAAAT)

ipdb> new_cst.location
    # 4-16

ipdb> new_cst.strand
    #  'from_location'
```

The `from_location` is strand argument is preserved, but the original strand in the location is lost, so the resulting constraint loses the strandedness.  I ran into this today when my optimization kept failing and the violated constraint was being reported on the positive strand even though that constraint should only be on the negative.

This was in version 3.2.6 (though the current code looks unchanged)

To fix this in the short term, it looks like I can specify the strand directly with its own argument when creating the constraint (let me know if this is not good for some reason).

This tool is very useful, thank you for the continued development!

Discussion:

Patch:
diff --git a/dnachisel/Location.py b/dnachisel/Location.py
index 853064f..6a3fdca 100644
--- a/dnachisel/Location.py
+++ b/dnachisel/Location.py
@@ -40,20 +40,25 @@ class Location:
 
     def overlap_region(self, other_location):
         """Return the overlap span between two locations (None if None)."""
-        strand = self.strand
         if other_location.start < self.start:
-            self, other_location = other_location, self
-
-        if other_location.start < self.end:
-            start = other_location.start
-            end = min(self.end, other_location.end)
-            strand = self.strand
-            return Location(start, end, strand)
+            left_location, right_location = other_location, self
         else:
+            left_location, right_location = self, other_location
+
+        if right_location.start >= left_location.end:
             return None
 
+        start = right_location.start
+        end = min(left_location.end, right_location.end)
+        return Location(start, end, self.strand)
+
     def extended(
-        self, extension_length, lower_limit=0, upper_limit=None, left=True, right=True,
+        self,
+        extension_length,
+        lower_limit=0,
+        upper_limit=None,
+        left=True,
+        right=True,
     ):
         """Extend the location of a few basepairs on each side."""
 
@@ -183,5 +188,7 @@ class Location:
         """Return a Biopython SeqFeature with same location and custom
         qualifiers."""
         return SeqFeature(
-            self.to_biopython_location(), type=feature_type, qualifiers=qualifiers,
+            self.to_biopython_location(),
+            type=feature_type,
+            qualifiers=qualifiers,
         )
diff --git a/dnachisel/builtin_specifications/AvoidPattern.py b/dnachisel/builtin_specifications/AvoidPattern.py
index e367e35..28642bd 100644
--- a/dnachisel/builtin_specifications/AvoidPattern.py
+++ b/dnachisel/builtin_specifications/AvoidPattern.py
@@ -38,9 +38,7 @@ class AvoidPattern(Specification):
     priority = 1
     shorthand_name = "no"  # will appear as, for instance, @no(BsmBI_site)
 
-    def __init__(
-        self, pattern=None, location=None, strand="from_location", boost=1.0
-    ):
+    def __init__(self, pattern=None, location=None, strand="from_location", boost=1.0):
         """Initialize."""
         if isinstance(pattern, str):
             pattern = SequencePattern.from_string(pattern)
@@ -72,7 +70,7 @@ class AvoidPattern(Specification):
             return "No %s" % self.pattern.name
         else:
             return "No %s" % self.pattern
-    
+
     def breach_label(self):
         if self.pattern.name is not None:
             return str(self.pattern.name)


Test patch:
diff --git a/tests/builtin_specifications/test_AvoidPattern.py b/tests/builtin_specifications/test_AvoidPattern.py
index 46633e7..bed081e 100644
--- a/tests/builtin_specifications/test_AvoidPattern.py
+++ b/tests/builtin_specifications/test_AvoidPattern.py
@@ -8,6 +8,7 @@ from dnachisel import (
     RepeatedKmerPattern,
     AvoidChanges,
     MotifPssmPattern,
+    Location,
 )
 import numpy
 
@@ -89,7 +90,9 @@ def test_AvoidPattern_on_strands():
     # Both strands
     sequence = "CATGCTATGC"
     problem = DnaOptimizationProblem(
-        sequence, constraints=[AvoidPattern("CAT")], logger=None,
+        sequence,
+        constraints=[AvoidPattern("CAT")],
+        logger=None,
     )
     problem.resolve_constraints()
     assert "CAT" not in problem.sequence
@@ -138,3 +141,10 @@ def test_AvoidPattern_with_regular_expression():
     assert not problem.all_constraints_pass()
     problem.resolve_constraints()
     assert problem.all_constraints_pass()
+
+
+def test_location_strand_gets_conserved():
+    cst = AvoidPattern("AACAAAT", Location(4, 1624, -1))
+    location = Location(9, 10)
+    new_cst = cst.localized(location)
+    assert new_cst.location.to_tuple() == (4, 16, -1)