Skip to content

Instantly share code, notes, and snippets.

@404Wolf
Last active March 29, 2023 04:57
Show Gist options
  • Save 404Wolf/bbbe24af9939124a0752b8909097e578 to your computer and use it in GitHub Desktop.
Save 404Wolf/bbbe24af9939124a0752b8909097e578 to your computer and use it in GitHub Desktop.
NATuG Junction Creation
def conjunct(self, NEMid1: NEMid, NEMid2: NEMid, skip_checks: bool = False) -> None:
"""
Create a cross-strand or same-strand junction between two NEMids.
The way that the conjuncting works depends on whether the NEMids are in the
same strands, and the closed-ness of the strands that the NEMids are in.
If the NEMids are in the same strand, and the strand is open, then a closed
loop will be formed and there will be a remainder open strand. If they are in
the same strand and the strand is closed, then the large closed loop will be
split into two smaller closed loops.
If the NEMids are in different strands, and one of the strands is open,
and one of the strands is closed then the closed loop strand will be opened
up and the open strand will remain open and grow longer. If they are in
different strands and both strands are open, then the new strands will both
be open and will extend in opposite directions from the junction site. If
they are in different strands and both strands are closed, then the two
closed loop strands will converge into one larger closed loop strand.
Args:
NEMid1: One NEMid at the junction site. Its x coordinate should be on the
integer line as NEMid2.
NEMid2: Another NEMid at the junction site. Its x coordinate should be on
the integer line as NEMid1.
skip_checks: Whether to skip checks for whether the junction is valid.
This is dangerous and should be reserved for debugging.
Raises:
ValueError: NEMids are ineligible to be made into a junction.
Notes:
- The order of NEMid1 and NEMid2 is arbitrary.
- NEMid.juncmate and NEMid.junction may be changed for NEMid1 and/or NEMid2.
- NEMid.matching may be changed based on whether the strand is closed or
not.
- The container Strands object will be restyled.
"""
# If skip checks is False, then skip checking for whether the junction is
# valid.
if not skip_checks:
# Ensure that both NEMids are both junctable.
if (not NEMid1.junctable) or (not NEMid2.junctable):
raise ValueError(
"NEMids are not both junctable.",
NEMid1,
NEMid2,
)
# Assert that the NEMids are indeed NEMids.
assert isinstance(NEMid1, NEMid), "NEMid1 is not a NEMid."
assert isinstance(NEMid2, NEMid), "NEMid2 is not a NEMid."
# Ensure that NEMid1 is the lefter NEMid. If it is not then swap the
# references to the NEMids.
if NEMid1.x_coord > NEMid2.x_coord:
NEMid1, NEMid2 = NEMid2, NEMid1
# Create two new strand objects. We will restyle all the strands at the end,
# so we do not need to worry about styling. In some cases we may not need to
# add two new strands, but we will still create two here, and may just not
# add one of them to the container later.
new_strands = [
Strand(nucleic_acid_profile=self.nucleic_acid_profile),
Strand(nucleic_acid_profile=self.nucleic_acid_profile),
]
# Log basic info for debugging.
logger.debug(f"NEMid1.strand is NEMid2.strand: {NEMid1.strand is NEMid2.strand}")
logger.debug(f"NEMid1.index={NEMid1.index}; NEMid2.index={NEMid2.index}")
logger.debug(f"NEMid1.closed={NEMid1.strand.closed}; NEMid2.closed={NEMid2.strand.closed}")
logger.debug(f"NEMid1-strand-length={len(NEMid1.strand)}; NEMid2-strand-length={len(NEMid2.strand)}")
# The two major cases are whether the NEMids are within the same strand,
# or are in different strands. We will start with the simple case of the
# NEMids being in the same strand.
if NEMid1.strand is NEMid2.strand:
# Create shorthand for strand since they are the same.
strand: Strand = NEMid1.strand # == NEMid2.strand
# Remove the old strand from the container. We remove it here as opposed
# to at the beginning/end of the function since we do not want to remove
# the same strand two times, and since we are already in the case that
# says that the strands are the same, know for sure that we only need to
# remove one strand.
self.remove(strand)
# This is the case where we are splitting a large closed loop in half.
if strand.closed:
# Crawl from the beginning of the strand to the junction site.
new_strands[0].items.extend(strand.sliced(0, NEMid1.index))
# Skip over all NEMids between NEMid 1's and NEMid 2's index
# and crawl from NEMid 2 to the end of the strand.
new_strands[0].items.extend(strand.sliced(NEMid2.index, None))
# Crawl from one junction site to the other for the other strand
new_strands[1].items.extend(strand.sliced(NEMid1.index, NEMid2.index))
# Since the large loop has been split in half, both the new strands
# are closed still.
new_strands[0].closed = True
new_strands[1].closed = True
# This is the case where we're creating a closed loop on an already
# conjuncted open strand.
elif not strand.closed:
# We want to start building the first new strand at the NEMid that is
# closer to the beginning of the strand, so that we progress down the
# strand until we reach the next NEMid. To ensure that this happens,
# we compare the indices of the NEMids.
if NEMid2.index < NEMid1.index:
# Crawl from the index of the right NEMid to the index of the
# left NEMid.
new_strands[0].items.extend(strand.sliced(NEMid2.index, NEMid1.index))
# Crawl from the beginning of the strand to the index of the
# right NEMid.
new_strands[1].items.extend(strand.sliced(0, NEMid2.index))
# Crawl from the index of the left NEMid to the end of the strand.
new_strands[1].items.extend(strand.sliced(NEMid1.index, None))
elif NEMid1.index < NEMid2.index:
# Crawl from the index of the left NEMid to the index of the
# right NEMid.
new_strands[0].items.extend(
strand.sliced(NEMid1.index, NEMid2.index)
)
# Crawl from the beginning of the strand to the index of the left
# NEMid.
new_strands[1].items.extend(strand.sliced(0, NEMid1.index))
# Crawl from the index of the right NEMid to the end of the strand.
new_strands[1].items.extend(strand.sliced(NEMid2.index, None))
# We have built the two new strands such that the new closed strand is
# new_strands[0] and the new open strand is new_strands[1].
new_strands[0].closed = True
new_strands[1].closed = False
# The second major case is when the NEMids are in different strands. This
# case is more complex, and has three different subcases.
elif NEMid1.strand is not NEMid2.strand:
# Remove the old strands from the parent container.
self.remove(NEMid1.strand)
self.remove(NEMid2.strand)
# The first subcase is when exactly one of the strands is closed,
# and the other is open. In this case a closed strand is being opedn up
# and joined with an open strand to become part of a longer open strand.
# Note that an `or` operator doesn't work for this because `or` allows for
# both strands to be closed, which is not what we want.
if (NEMid1.strand.closed, NEMid2.strand.closed).count(True) == 1:
# Create references for the stand that is closed/the strand that is
# open so that we can use them in the code below.
if NEMid1.strand.closed:
closed_strand_NEMid = NEMid1
open_strand_NEMid = NEMid2
elif NEMid2.strand.closed:
closed_strand_NEMid = NEMid2
open_strand_NEMid = NEMid1
# Crawl from beginning of the open strand to the junction site NEMid
# of the open strand.
new_strands[0].items.extend(open_strand_NEMid.strand.sliced(0, open_strand_NEMid.index))
# Crawl from the junction site's closed strand NEMid to the end of
# the closed strand.
new_strands[0].items.extend(closed_strand_NEMid.strand.sliced(closed_strand_NEMid.index, None))
# Crawl from the beginning of the closed strand to the junction site
# of the closed strand.
new_strands[0].items.extend(closed_strand_NEMid.strand.sliced(0, closed_strand_NEMid.index))
# Crawl from the junction site of the open strand to the end of the
# open strand.
new_strands[0].items.extend(open_strand_NEMid.strand.sliced(open_strand_NEMid.index, None))
# Since we've created only one strand, and it is just a longer open
# strand, we set new_strands[0] to be open.
new_strands[0].closed = False
# If both of the NEMids are in different closed strands, then we are
# going to be making one larger closed strand.
elif NEMid1.strand.closed and NEMid2.strand.closed:
# Convert the strands to deques so that they can be rotated.
NEMid1.strand.items = deque(NEMid1.strand.items)
NEMid2.strand.items = deque(NEMid2.strand.items)
# Make it so that the strands both begin at the respective NEMids.
for NEMid_ in (NEMid1, NEMid2):
NEMid_.strand.items.rotate(len(NEMid_.strand) - 1 - NEMid_.index)
# Convert the strands back to StrandItems.
NEMid1.strand.items = StrandItems(NEMid1.strand.items)
NEMid2.strand.items = StrandItems(NEMid2.strand.items)
# Add the entire first reordered strand to the new strand.
new_strands[0].items.extend(NEMid1.strand.items)
# Add the entire second reordered strand to the new strand.
new_strands[0].items.extend(NEMid2.strand.items)
# Since we've created one larger closed strand, we set new_strands[0]
# to be closed.
new_strands[0].closed = True
# If both strands are open then we will be converting two already open
# strands into two new open strands that traverse each others' domains.
elif (not NEMid1.strand.closed) and (not NEMid2.strand.closed):
# Crawl from beginning of NEMid#1's strand to the junction site.
new_strands[0].items.extend(NEMid1.strand.sliced(0, NEMid1.index))
# Crawl from the junction site on NEMid#2's strand to the end of the
# strand.
new_strands[0].items.extend(NEMid2.strand.sliced(NEMid2.index, None))
# Crawl from the beginning of NEMid#2's strand to the junction site.
new_strands[1].items.extend(NEMid2.strand.sliced(0, NEMid2.index))
# Crawl from the junction on NEMid #1's strand to the end of the strand.
new_strands[1].items.extend(NEMid1.strand.sliced(NEMid1.index, None))
# Since both were originally open strands, we set both new strands to
# be open since they are just differently arranged open strands.
new_strands[0].closed = False
new_strands[1].closed = False
# Append all the new strands that were created to the parent container.
# We can check if a strand is empty to see if we've constructed it.
for new_strand in new_strands:
if not new_strand.empty:
self.append(new_strand)
# Set the parent of all the items in the new strands to their proper new strand.
for new_strand in new_strands:
for item in new_strand.items:
item.strand = new_strand
# If the new strand of NEMid#1 or NEMid#2 doesn't leave its domain
# then mark NEMid1 as not-a-junction
for NEMid_ in (NEMid1, NEMid2):
if NEMid_.strand.interdomain:
NEMid_.junction = True
else:
NEMid_.junction = False
# Assign the NEMids as juncmates now that we've created a junction between them.
# To assign them as juncmates means to assign their juncmate attribute to a
# reference to the other NEMid.
NEMid1.juncmate = NEMid2
NEMid2.juncmate = NEMid1
# Restyle the Strands container. This will cause all the strands and points
# within the strands in the parent container to also be restyled accordingly.
self.style()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment