Skip to content

Closes #2; support Extensions on the end of a Strand#230

Merged
dave-doty merged 47 commits intodevfrom
2-support-loopouts-on-the-end-of-a-strand
Jun 9, 2022
Merged

Closes #2; support Extensions on the end of a Strand#230
dave-doty merged 47 commits intodevfrom
2-support-loopouts-on-the-end-of-a-strand

Conversation

@UnHumbleBen
Copy link
Copy Markdown
Collaborator

WIP: #2

@dave-doty I added some unit tests for chain methods for DNA extensions. Can you give feedback on the unit tests written so far. Is the tested behavior desirable? Are there additional tests/behavior you would like me to add?

@UnHumbleBen
Copy link
Copy Markdown
Collaborator Author

UnHumbleBen commented May 4, 2022

@dave-doty For creating a 5' extension, should the extension chain method only take effect when a domain is added, similar to how cross works? Or should it just add an extension immediately?

If called before any domains have been created, then the extension will only be added
when a domain has been created.

@dave-doty
Copy link
Copy Markdown
Member

For creating a 5' extension, should the extension chain method only take effect when a domain is added, similar to how cross works? Or should it just add an extension immediately?

It should be like move or to and make the extension immediately; in particular, if it is the last substrand, it would be created by having the extension chained method be the last method called, so it would have to create it immediately. (But there's no reason not to also create it immediately if it is the first chained method call.) This should be legit:

design.draw_strand(0,0).move(10).cross(1).move(-5).extension(6)

On the other hand, if it is the first substrand, it's not clear what's the best approach. draw_strand takes a helix and offset as input. It would be awkward if those arguments refer to something other than the first substrand on the strand. Any ideas for how to do this in a clean, readable way that won't be confusing? (And that preserves the behavior we currently have if there are no extensions?)

@dave-doty
Copy link
Copy Markdown
Member

Any ideas for how to do this in a clean, readable way that won't be confusing? (And that preserves the behavior we currently have if there are no extensions?)

After thinking about this for a second, I think the best is to have an optional argument extension_5p_length in draw_strand. To create a 5' extension of length 6, followed by a domain on helix 0 from offset 0 to 9, you'd do this:

design.draw_strand(0, 0, extension_5p_length=6).move(10)

Then maybe the name of the extension chained method should be changed to extension_3p to emphasize that it can only be used to create a 3' extension.

@UnHumbleBen
Copy link
Copy Markdown
Collaborator Author

UnHumbleBen commented May 6, 2022

Any ideas for how to do this in a clean, readable way that won't be confusing? (And that preserves the behavior we currently have if there are no extensions?)

After thinking about this for a second, I think the best is to have an optional argument extension_5p_length in draw_strand. To create a 5' extension of length 6, followed by a domain on helix 0 from offset 0 to 9, you'd do this:

design.draw_strand(0, 0, extension_5p_length=6).move(10)

Then maybe the name of the extension chained method should be changed to extension_3p to emphasize that it can only be used to create a 3' extension.

I think this is a good idea and I'll proceed with implementing this way.

The only thing I'm unsure about is how to set a default value for the 5' extension relative_offset. Suppose the following snippet happens:

design.draw_strand(0, 0, extension_5p_length=6)

Sure, we can create an Extension object, but what relative_offset should it have? If it were a 3' extension, this problem would be simple, either (1, -1) for forward strand and (-1, 1) for reverse strand. But for the 5' extension, we don't know whether the domain will be created in the forward or reverse direction, so we can't assign a default relative_offset. I suppose we could make the relative_offset field optional and have the empty value denote that the default placement is desired. What do you think @dave-doty?

Edit: Disregard the question above, I just realize that we have always been using the absence of a value to indicate default so there's no need to explicitly figure out what the offset is and just assign None to it and make relative_offset an Optional field

@UnHumbleBen
Copy link
Copy Markdown
Collaborator Author

UnHumbleBen commented May 16, 2022

@dave-doty Almost finished implementation in the Python library. Just wanted to double check a few things with you. I have 3 test cases for actions that I think should be disallowed and wanted to see if you agree.

def test_ligate_on_extension_side_should_error(self) -> None:
"""
/
/
[-------[----->
^
|
error to ligate here
"""

def test_add_full_crossover_on_extension_error(self) -> None:
"""
Before:
/
/
/
0 [------- [------>
1 <------] <------]
Error:
/
/
/
0 [------+ +------>
| |
1 <------+ +------]
"""

def test_add_half_crossover_on_extension_error(self) -> None:
"""
Before:
\
\
0 ------->
1 <------]
Error:
\
\
0 +------>
|
1 +------]
"""

Essentially, should ligate, crossover, and half-crossover be disallowed if applied to the domain on the side where the extension is located.

I also have test cases for normal cases for ligate, crossover, and nick. All the error cases and normal cases begin here, if you want to take a look:

def test_ligate_on_extension_side_should_error(self) -> None:

@dave-doty
Copy link
Copy Markdown
Member

Yes, I agree those cases should all be errors.

@UnHumbleBen
Copy link
Copy Markdown
Collaborator Author

UnHumbleBen commented May 19, 2022

Discovered something strange about ligate while implementing the ligate error checking. There's no check on Python that the domains to ligate are at the end of the strand. For example something like this:
image
Clicking on helix 0, offset 15/16 should not be allowed. This check is done in the web app (nothing happens when I click there on ligate mode). However, this check is not implemented in the Python library:

Test case:

    def test_ligate_middle(self) -> None:
        """
        [-----+[----->
              |
        <-----+
        """
        design: sc.Design = sc.Design(helices=[sc.Helix(max_offset=100), sc.Helix(max_offset=100)])
        design.draw_strand(0, 0).to(10).cross(1).to(0)
        design.draw_strand(0, 10).to(20)

        design.ligate(0, 10, True)

Debugging design, I see the resulting strand object contains two overlapping domains, which is nonsensical:

 "strands": [
    {
      "color": "#f74308",
      "domains": [
        {"helix": 0, "forward": true, "start": 0, "end": 10},
        {"helix": 0, "forward": true, "start": 0, "end": 20}
      ]
    }
  ]

Is there some work that needs to be done with the existing ligate function to prevent this?

@UnHumbleBen
Copy link
Copy Markdown
Collaborator Author

I notice a similar problem with crossovers:

In the example here, adding a crossover between 1 and 2 at offset 0 should not be allowed. The web app prevents this (because of the modify crossover feature) but there are no checks in the python code:

    def test_add_half_crossover_middle(self) -> None:
        """
        0  +------]
           |
        1  +------>

        2  <------]
        """
        # Setup
        design: sc.Design = sc.Design(
            helices=[sc.Helix(max_offset=100), sc.Helix(max_offset=100), sc.Helix(max_offset=100)]
        )
        design.draw_strand(0, 10).to(0).cross(1).to(10)
        design.draw_strand(2, 10).to(0)

        design.add_half_crossover(1, 2, 0, True)

The code should throw an error but instead, we get a new design with strands:

  "strands": [
    {
      "color": "#57bb00",
      "domains": [
        {"helix": 2, "forward": false, "start": 0, "end": 10},
        {"helix": 0, "forward": false, "start": 0, "end": 10},
        {"helix": 1, "forward": true, "start": 0, "end": 10}
      ]
    }
  ]

which visually looks like:

image

which isn't right either. Am I using these functions incorrectly or are there actual bugs?

@dave-doty
Copy link
Copy Markdown
Member

Is there some work that needs to be done with the existing ligate function to prevent this?

Definitely looks like an error to me. Do you mind adding unit tests to ensure it raises an exception?

@dave-doty
Copy link
Copy Markdown
Member

Am I using these functions incorrectly or are there actual bugs?

This also looks like a bug.

@UnHumbleBen UnHumbleBen marked this pull request as ready for review May 23, 2022 07:02
@UnHumbleBen
Copy link
Copy Markdown
Collaborator Author

@dave-doty Changes are ready to merge to dev now

@UnHumbleBen UnHumbleBen changed the title WIP: Closes #2; support Extensions on the end of a Strand Closes #2; support Extensions on the end of a Strand May 23, 2022
@dave-doty
Copy link
Copy Markdown
Member

Can you write up some simple release notes and put them as a comment in the related issue #2? That way we can copy/paste them into the release notes when this is merged to main.

@dave-doty dave-doty merged commit 3c47e6d into dev Jun 9, 2022
@dave-doty dave-doty deleted the 2-support-loopouts-on-the-end-of-a-strand branch June 9, 2022 19:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants