diff --git a/examples/output_designs/domain_colors.sc b/examples/output_designs/domain_colors.sc index bb4cff75..c7a36c39 100644 --- a/examples/output_designs/domain_colors.sc +++ b/examples/output_designs/domain_colors.sc @@ -1,24 +1,24 @@ -{ - "version": "0.17.7", - "grid": "square", - "helices": [ - {"max_offset": 100, "grid_position": [0, 0]}, - {"max_offset": 100, "grid_position": [0, 1]}, - {"max_offset": 100, "grid_position": [0, 2]}, - {"max_offset": 100, "grid_position": [0, 3]} - ], - "strands": [ - { - "color": "#0000ff", - "domains": [ - {"extension_num_bases": 5, "color": "#ff0000"}, - {"helix": 0, "forward": true, "start": 0, "end": 8, "color": "#00ff00"}, - {"loopout": 5, "color": "#000096"}, - {"helix": 1, "forward": false, "start": 0, "end": 8, "color": "#960000"}, - {"helix": 2, "forward": true, "start": 0, "end": 8, "color": "#009600"}, - {"helix": 3, "forward": false, "start": 0, "end": 8}, - {"extension_num_bases": 5, "color": "#000000"} - ] - } - ] +{ + "version": "0.17.8", + "grid": "square", + "helices": [ + {"max_offset": 100, "grid_position": [0, 0]}, + {"max_offset": 100, "grid_position": [0, 1]}, + {"max_offset": 100, "grid_position": [0, 2]}, + {"max_offset": 100, "grid_position": [0, 3]} + ], + "strands": [ + { + "color": "#0000ff", + "domains": [ + {"extension_num_bases": 5, "color": "#ff0000"}, + {"helix": 0, "forward": true, "start": 0, "end": 8, "color": "#00ff00"}, + {"loopout": 5, "color": "#000096"}, + {"helix": 1, "forward": false, "start": 0, "end": 8, "color": "#960000"}, + {"helix": 2, "forward": true, "start": 0, "end": 8, "color": "#009600"}, + {"helix": 3, "forward": false, "start": 0, "end": 8}, + {"extension_num_bases": 5, "color": "#000000"} + ] + } + ] } \ No newline at end of file diff --git a/examples/output_designs/oxdna_export_with_pitch.sc b/examples/output_designs/oxdna_export_with_pitch.sc new file mode 100644 index 00000000..984c6e96 --- /dev/null +++ b/examples/output_designs/oxdna_export_with_pitch.sc @@ -0,0 +1,28 @@ +{ + "version": "0.17.8", + "groups": { + "default_group": { + "position": {"x": 0, "y": 0, "z": 0}, + "pitch": 45, + "grid": "square" + } + }, + "helices": [ + {"max_offset": 100, "grid_position": [0, 0]}, + {"max_offset": 100, "grid_position": [0, 1]} + ], + "strands": [ + { + "color": "#f74308", + "domains": [ + {"helix": 0, "forward": true, "start": 0, "end": 21} + ] + }, + { + "color": "#57bb00", + "domains": [ + {"helix": 1, "forward": true, "start": 0, "end": 21} + ] + } + ] +} \ No newline at end of file diff --git a/examples/output_designs/oxdna_export_with_pitch.zip b/examples/output_designs/oxdna_export_with_pitch.zip new file mode 100644 index 00000000..b720d87d Binary files /dev/null and b/examples/output_designs/oxdna_export_with_pitch.zip differ diff --git a/examples/output_designs/oxdna_export_with_pitch_0.sc b/examples/output_designs/oxdna_export_with_pitch_0.sc new file mode 100644 index 00000000..be927a02 --- /dev/null +++ b/examples/output_designs/oxdna_export_with_pitch_0.sc @@ -0,0 +1,22 @@ +{ + "version": "0.17.8", + "grid": "square", + "helices": [ + {"max_offset": 100, "grid_position": [0, 0]}, + {"max_offset": 100, "grid_position": [0, 1]} + ], + "strands": [ + { + "color": "#f74308", + "domains": [ + {"helix": 0, "forward": true, "start": 0, "end": 21} + ] + }, + { + "color": "#57bb00", + "domains": [ + {"helix": 1, "forward": true, "start": 0, "end": 21} + ] + } + ] +} \ No newline at end of file diff --git a/examples/output_designs/oxdna_export_with_pitch_45.sc b/examples/output_designs/oxdna_export_with_pitch_45.sc new file mode 100644 index 00000000..984c6e96 --- /dev/null +++ b/examples/output_designs/oxdna_export_with_pitch_45.sc @@ -0,0 +1,28 @@ +{ + "version": "0.17.8", + "groups": { + "default_group": { + "position": {"x": 0, "y": 0, "z": 0}, + "pitch": 45, + "grid": "square" + } + }, + "helices": [ + {"max_offset": 100, "grid_position": [0, 0]}, + {"max_offset": 100, "grid_position": [0, 1]} + ], + "strands": [ + { + "color": "#f74308", + "domains": [ + {"helix": 0, "forward": true, "start": 0, "end": 21} + ] + }, + { + "color": "#57bb00", + "domains": [ + {"helix": 1, "forward": true, "start": 0, "end": 21} + ] + } + ] +} \ No newline at end of file diff --git a/examples/oxdna/oxdna_export_with_pitch_0.dat b/examples/oxdna/oxdna_export_with_pitch_0.dat new file mode 100644 index 00000000..2e041153 --- /dev/null +++ b/examples/oxdna/oxdna_export_with_pitch_0.dat @@ -0,0 +1,45 @@ +t = 0 +b = 19.19288565390937 19.19288565390937 19.19288565390937 +E = 0 0 0 +-0.20521208599540122 -0.563815572471545 0.0 0.3420201433256687 0.9396926207859084 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.14805443861417622 -0.5814463717374467 0.3897628551303123 -0.2467573976902937 0.969077286229078 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.4498687217806405 -0.39701150255811557 0.7795257102606246 -0.7497812029677342 0.6616858375968593 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.5953435239601033 -0.07460622278849112 1.1692885653909368 -0.9922392066001721 0.1243437046474852 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.5339230852868811 0.2737263944118978 1.5590514205212491 -0.8898718088114685 -0.456210657353163 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.2869523871727911 0.5269329440221371 1.9488142756515616 -0.47825397862131847 -0.8782215733702284 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.059740707957489946 0.5970184652192408 2.3385771307818737 0.09956784659581658 -0.9950307753654014 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.3856725658119234 0.459626665871387 2.728339985912186 0.642787609686539 -0.7660444431189783 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.5775745481700072 0.16250428088580296 3.1181028410424982 0.962624246950012 -0.27084046814300494 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.568756407700279 -0.19109199015101058 3.5078656961728107 0.9479273461671318 0.3184866502516843 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.36228264619528666 -0.47827950433375327 3.8976285513031232 0.6038044103254778 0.7971325072229222 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.02990753139641875 -0.5992541527313534 4.287391406433435 0.04984588566069792 0.9987569212189223 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.31286112202769867 -0.5119745289792934 4.677154261563747 -0.5214352033794978 0.8532908816321557 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.5469035113870039 -0.24677226187836673 5.06691711669406 -0.9115058523116732 0.41128710313061123 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.590884651807325 0.10418890660015769 5.456679971824372 -0.9848077530122084 -0.17364817766692947 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.429520109555832 0.41894209085164286 5.846442826954685 -0.71586684925972 -0.6982368180860714 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.11888768591963841 0.5881034927091263 6.2362056820849965 -0.19814614319939736 -0.9801724878485439 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.23306087776481726 0.5528857271222444 6.625968537215309 0.38843479627469546 -0.9214762118704074 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.5040155538904627 0.32552775831945585 7.0157313923456215 0.8400259231507712 -0.5425462638657598 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.5998135092004897 -0.014958415042842765 7.405494247475934 0.9996891820008162 0.024930691738071276 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.48716280342951424 -0.3502462033408734 7.7952571026062465 0.8119380057158571 0.5837436722347891 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.20521208599540122 2.95813793774212 0.0 0.3420201433256687 0.9396926207859084 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.14805443861417622 2.9405071384762183 0.3897628551303123 -0.2467573976902937 0.969077286229078 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.4498687217806405 3.1249420076555494 0.7795257102606246 -0.7497812029677342 0.6616858375968593 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.5953435239601033 3.447347287425174 1.1692885653909368 -0.9922392066001721 0.1243437046474852 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.5339230852868811 3.7956799046255627 1.5590514205212491 -0.8898718088114685 -0.456210657353163 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.2869523871727911 4.048886454235802 1.9488142756515616 -0.47825397862131847 -0.8782215733702284 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.059740707957489946 4.118971975432906 2.3385771307818737 0.09956784659581658 -0.9950307753654014 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.3856725658119234 3.981580176085052 2.728339985912186 0.642787609686539 -0.7660444431189783 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.5775745481700072 3.684457791099468 3.1181028410424982 0.962624246950012 -0.27084046814300494 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.568756407700279 3.3308615200626543 3.5078656961728107 0.9479273461671318 0.3184866502516843 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.36228264619528666 3.043674005879912 3.8976285513031232 0.6038044103254778 0.7971325072229222 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.02990753139641875 2.9226993574823115 4.287391406433435 0.04984588566069792 0.9987569212189223 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.31286112202769867 3.0099789812343714 4.677154261563747 -0.5214352033794978 0.8532908816321557 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.5469035113870039 3.2751812483352984 5.06691711669406 -0.9115058523116732 0.41128710313061123 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.590884651807325 3.6261424168138228 5.456679971824372 -0.9848077530122084 -0.17364817766692947 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.429520109555832 3.940895601065308 5.846442826954685 -0.71586684925972 -0.6982368180860714 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +0.11888768591963841 4.110057002922791 6.2362056820849965 -0.19814614319939736 -0.9801724878485439 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.23306087776481726 4.074839237335909 6.625968537215309 0.38843479627469546 -0.9214762118704074 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.5040155538904627 3.847481268533121 7.0157313923456215 0.8400259231507712 -0.5425462638657598 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.5998135092004897 3.506995095170822 7.405494247475934 0.9996891820008162 0.024930691738071276 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 +-0.48716280342951424 3.1717073068727917 7.7952571026062465 0.8119380057158571 0.5837436722347891 -0.0 -0.0 -0.0 -1.0 0 0 0 0 0 0 diff --git a/examples/oxdna/oxdna_export_with_pitch_0.top b/examples/oxdna/oxdna_export_with_pitch_0.top new file mode 100644 index 00000000..df11796b --- /dev/null +++ b/examples/oxdna/oxdna_export_with_pitch_0.top @@ -0,0 +1,43 @@ +42 2 +1 T 1 -1 +1 T 2 0 +1 T 3 1 +1 T 4 2 +1 T 5 3 +1 T 6 4 +1 T 7 5 +1 T 8 6 +1 T 9 7 +1 T 10 8 +1 T 11 9 +1 T 12 10 +1 T 13 11 +1 T 14 12 +1 T 15 13 +1 T 16 14 +1 T 17 15 +1 T 18 16 +1 T 19 17 +1 T 20 18 +1 T -1 19 +2 T 22 -1 +2 T 23 21 +2 T 24 22 +2 T 25 23 +2 T 26 24 +2 T 27 25 +2 T 28 26 +2 T 29 27 +2 T 30 28 +2 T 31 29 +2 T 32 30 +2 T 33 31 +2 T 34 32 +2 T 35 33 +2 T 36 34 +2 T 37 35 +2 T 38 36 +2 T 39 37 +2 T 40 38 +2 T 41 39 +2 T -1 40 diff --git a/examples/oxdna/oxdna_export_with_pitch_45.dat b/examples/oxdna/oxdna_export_with_pitch_45.dat new file mode 100644 index 00000000..25b360cc --- /dev/null +++ b/examples/oxdna/oxdna_export_with_pitch_45.dat @@ -0,0 +1,45 @@ +t = 0 +b = 21.051049002838713 21.051049002838713 21.051049002838713 +E = 0 0 0 +-0.20521208599540125 -0.3986778146332049 0.3986778146332049 0.34202014332566877 0.6644630243886749 -0.6644630243886749 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.14805443861417625 -0.1355407144345891 0.6867486302691366 -0.24675739769029376 0.6852411205864382 -0.6852411205864382 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.4498687217806406 0.27047839016664355 0.8319374415024515 -0.7497812029677343 0.46788254277984 -0.46788254277984 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.5953435239601033 0.774057307699365 0.8795664398042778 -0.9922392066001722 0.08792427675409403 -0.08792427675409403 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.5339230852868813 1.2959696213474916 0.9088620419906985 -0.8898718088114688 -0.32258964946399415 0.32258964946399415 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.2869523871727911 1.7506176475350137 1.0054219316377244 -0.4782539786213185 -0.6209964299144077 0.6209964299144077 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.05974070795748991 2.075779552753753 1.2314679422535326 0.09956784659581652 -0.7035930087501837 0.7035930087501837 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.3856725658119235 2.254232837672738 1.6042225731690951 0.6427876096865391 -0.5416752204197022 0.5416752204197022 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.5775745481700073 2.3197395423243847 2.0899237843519956 0.9626242469500121 -0.19151313164365794 0.19151313164365794 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.5687564077002791 2.345313179189252 2.615558063321677 0.9479273461671319 0.22520407011035426 -0.22520407011035426 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.36228264619528666 2.4178448983558005 3.0942342599896757 0.6038044103254778 0.563657801361563 -0.563657801361563 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.029907531396418784 2.6079068620394725 3.4553802121405504 0.049845885660697975 0.7062277917508986 -0.7062277917508986 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.3128611220276987 2.9452268337712386 3.6692681562433327 -0.5214352033794979 0.603367768726745 -0.603367768726745 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.546903511387004 3.4083571131416237 3.757345792707495 -0.9115058523116734 0.2908238996382262 -0.2908238996382262 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.590884651807325 3.9321280932232163 3.784782728460449 -0.9848077530122084 -0.12278780396897232 0.12278780396897232 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.429520109555832 4.430296162124774 3.837822575393439 -0.71586684925972 -0.493727988942779 0.493727988942779 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.11888768591963843 4.825515294410497 3.9938113589422635 -0.19814614319939738 -0.6930866128901945 0.6930866128901945 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.23306087776481724 5.076216531463048 4.29431803772426 0.3884347962746954 -0.6515820781156569 0.6515820781156569 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.5040155538904627 5.1910541278830715 4.730688357138786 0.8400259231507713 -0.38363814228690474 0.38363814228690474 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.5998135092004898 5.225898003715605 5.2470523971407985 0.9996891820008165 0.017628661187661654 -0.017628661187661654 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.4871628034295143 5.264417692878302 5.75974062381265 0.8119380057158572 0.41276910911195674 -0.41276910911195674 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.20521208599540125 3.12327569558046 0.3986778146332049 0.34202014332566877 0.6644630243886749 -0.6644630243886749 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.14805443861417625 3.386412795779076 0.6867486302691366 -0.24675739769029376 0.6852411205864382 -0.6852411205864382 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.4498687217806406 3.7924319003803086 0.8319374415024515 -0.7497812029677343 0.46788254277984 -0.46788254277984 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.5953435239601033 4.29601081791303 0.8795664398042778 -0.9922392066001722 0.08792427675409403 -0.08792427675409403 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.5339230852868813 4.817923131561156 0.9088620419906985 -0.8898718088114688 -0.32258964946399415 0.32258964946399415 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.2869523871727911 5.272571157748678 1.0054219316377244 -0.4782539786213185 -0.6209964299144077 0.6209964299144077 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.05974070795748991 5.597733062967418 1.2314679422535326 0.09956784659581652 -0.7035930087501837 0.7035930087501837 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.3856725658119235 5.776186347886402 1.6042225731690951 0.6427876096865391 -0.5416752204197022 0.5416752204197022 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.5775745481700073 5.84169305253805 2.0899237843519956 0.9626242469500121 -0.19151313164365794 0.19151313164365794 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.5687564077002791 5.867266689402917 2.615558063321677 0.9479273461671319 0.22520407011035426 -0.22520407011035426 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.36228264619528666 5.939798408569465 3.0942342599896757 0.6038044103254778 0.563657801361563 -0.563657801361563 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.029907531396418784 6.1298603722531375 3.4553802121405504 0.049845885660697975 0.7062277917508986 -0.7062277917508986 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.3128611220276987 6.467180343984904 3.6692681562433327 -0.5214352033794979 0.603367768726745 -0.603367768726745 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.546903511387004 6.93031062335529 3.757345792707495 -0.9115058523116734 0.2908238996382262 -0.2908238996382262 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.590884651807325 7.454081603436881 3.784782728460449 -0.9848077530122084 -0.12278780396897232 0.12278780396897232 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.429520109555832 7.952249672338439 3.837822575393439 -0.71586684925972 -0.493727988942779 0.493727988942779 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +0.11888768591963843 8.347468804624162 3.9938113589422635 -0.19814614319939738 -0.6930866128901945 0.6930866128901945 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.23306087776481724 8.598170041676713 4.29431803772426 0.3884347962746954 -0.6515820781156569 0.6515820781156569 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.5040155538904627 8.713007638096736 4.730688357138786 0.8400259231507713 -0.38363814228690474 0.38363814228690474 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.5998135092004898 8.747851513929268 5.2470523971407985 0.9996891820008165 0.017628661187661654 -0.017628661187661654 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 +-0.4871628034295143 8.786371203091967 5.75974062381265 0.8119380057158572 0.41276910911195674 -0.41276910911195674 -0.0 -0.7071067811865476 -0.7071067811865476 0 0 0 0 0 0 diff --git a/examples/oxdna/oxdna_export_with_pitch_45.top b/examples/oxdna/oxdna_export_with_pitch_45.top new file mode 100644 index 00000000..df11796b --- /dev/null +++ b/examples/oxdna/oxdna_export_with_pitch_45.top @@ -0,0 +1,43 @@ +42 2 +1 T 1 -1 +1 T 2 0 +1 T 3 1 +1 T 4 2 +1 T 5 3 +1 T 6 4 +1 T 7 5 +1 T 8 6 +1 T 9 7 +1 T 10 8 +1 T 11 9 +1 T 12 10 +1 T 13 11 +1 T 14 12 +1 T 15 13 +1 T 16 14 +1 T 17 15 +1 T 18 16 +1 T 19 17 +1 T 20 18 +1 T -1 19 +2 T 22 -1 +2 T 23 21 +2 T 24 22 +2 T 25 23 +2 T 26 24 +2 T 27 25 +2 T 28 26 +2 T 29 27 +2 T 30 28 +2 T 31 29 +2 T 32 30 +2 T 33 31 +2 T 34 32 +2 T 35 33 +2 T 36 34 +2 T 37 35 +2 T 38 36 +2 T 39 37 +2 T 40 38 +2 T 41 39 +2 T -1 40 diff --git a/examples/oxdna_export_with_pitch.py b/examples/oxdna_export_with_pitch.py new file mode 100644 index 00000000..a0ecf385 --- /dev/null +++ b/examples/oxdna_export_with_pitch.py @@ -0,0 +1,20 @@ +import scadnano as sc + + +def create_design(pitch: float) -> sc.Design: + helices = [sc.Helix(max_offset=100) for _ in range(2)] + design = sc.Design(helices=helices, grid=sc.square) + + design.groups[sc.default_group_name].pitch = pitch + + design.draw_strand(0, 0).to(21) + design.draw_strand(1, 0).to(21) + + return design + + +if __name__ == '__main__': + for pitch in [0, 45]: + d = create_design(pitch) + d.write_scadnano_file(filename=f'oxdna_export_with_pitch_{pitch}.sc', directory='output_designs') + d.write_oxdna_files(filename_no_extension=f'oxdna_export_with_pitch_{pitch}', directory='oxdna') diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 7128d829..a4b73ff8 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -51,10 +51,9 @@ """ # needed to use forward annotations: https://docs.python.org/3/whatsnew/3.7.html#whatsnew37-pep563 -# commented out for now to support Py3.6, which does not support this feature -# from __future__ import annotations +from __future__ import annotations -__version__ = "0.17.7" # version line; WARNING: do not remove or change this line or comment +__version__ = "0.18.0" # version line; WARNING: do not remove or change this line or comment import collections import dataclasses @@ -66,7 +65,7 @@ from builtins import ValueError from dataclasses import dataclass, field, InitVar, replace from typing import Iterator, Tuple, List, Sequence, Iterable, Set, Dict, Union, Optional, Type, cast, Any, \ - TypeVar, Generic, Callable, AbstractSet, Deque + TypeVar, Callable, AbstractSet from collections import defaultdict, OrderedDict, Counter import sys import os.path @@ -456,6 +455,12 @@ class M13Variant(enum.Enum): https://www.tilibit.com/collections/scaffold-dna/products/single-stranded-scaffold-dna-type-p8064 """ + p8634 = "p8634" + """Variant of M13mp18 that is 8634 bases long. At the time of this writing, not listed as available + from any biotech vender, but Tilibit will make it for you if you ask. + (https://www.tilibit.com/pages/contact-us) + """ + def m13(rotation: int = 5587, variant: M13Variant = M13Variant.p7249) -> str: """ @@ -732,10 +737,101 @@ def m13(rotation: int = 5587, variant: M13Variant = M13Variant.p7249) -> str: TATGATTGACATGCTAGTTTTACGATTACCGTTCATCGATTCTCTTGTTTGCTCCAGACTCTCA ''') +_8634 = re.sub(r'\s', '', ''' +GAGTCCACGTTCTTTAATAGTGGACTCTTGTTCCAAACTGGAACAACACTCAACCCTATCTCGGGCTATTCTTTTGATTTATAAGGGATTTTGCCGATTT +CGGAACCACCATCAAACAGGATTTTCGCCTGCTGGGGCAAACCAGCGTGGACCGCTTGCTGCAACTCTCTCAGGGCCAGGCGGTGAAGGGCAATCAGCTG +TTGCCCGTCTCACTGGTGAAAAGAAAAACCACCCTGGCGCCCAATACGCAAACCGCCTCTCCCCGCGCGTTGGCCGATTCATTAATGCAGCTGGCACGAC +AGGTTTCCCGACTGGAAAGCGGGCAGTGAGCGCAACGCAATTAATGTGAGTTAGCTCACTCATTAGGCACCCCAGGCTTTACACTTTATGCTTCCGGCTC +GTATGTTGTGTGGAATTGTGAGCGGATAACAATTTCACACAGGAAACAGCTATGACCATGATTACGAATTCGAGCTCGGTACCCGGGGATCCATTCTCCT +GTGACTCGGAAGTGCATTTATCATCTCCATAAAACAAAACCCGCCGTAGCGAGTTCAGATAAAATAAATCCCCGCGAGTGCGAGGATTGTTATGTAATAT +TGGGTTTAATCATCTATATGTTTTGTACAGAGAGGGCAAGTATCGTTTCCACCGTACTCGTGATAATAATTTTGCACGGTATCAGTCATTTCTCGCACAT +TGCAGAATGGGGATTTGTCTTCATTAGACTTATAAACCTTCATGGAATATTTGTATGCCGACTCTATATCTATACCTTCATCTACATAAACACCTTCGTG +ATGTCTGCATGGAGACAAGACACCGGATCTGCACAACATTGATAACGCCCAATCTTTTTGCTCAGACTCTAACTCATTGATACTCATTTATAAACTCCTT +GCAATGTATGTCGTTTCAGCTAAACGGTATCAGCAATGTTTATGTAAAGAAACAGTAAGATAATACTCAACCCGATGTTTGAGTACGGTCATCATCTGAC +ACTACAGACTCTGGCATCGCTGTGAAGACGACGCGAAATTCAGCATTTTCACAAGCGTTATCTTTTACAAAACCGATCTCACTCTCCTTTGATGCGAATG +CCAGCGTCAGACATCATATGCAGATACTCACCTGCATCCTGAACCCATTGACCTCCAACCCCGTAATAGCGATGCGTAATGATGTCGATAGTTACTAACG +GGTCTTGTTCGATTAACTGCCGCAGAAACTCTTCCAGGTCACCAGTGCAGTGCTTGATAACAGGAGTCTTCCCAGGATGGCGAACAACAAGAAACTGGTT +TCCGTCTTCACGGACTTCGTTGCTTTCCAGTTTAGCAATACGCTTACTCCCATCCGAGATAACACCTTCGTAATACTCACGCTGCTCGTTGAGTTTTGAT +TTTGCTGTTTCAAGCTCAACACGCAGTTTCCCTACTGTTAGCGCAATATCCTCGTTCTCCTGGTCGCGGCGTTTGATGTATTGCTGGTTTCTTTCCCGTT +CATCCAGCAGTTCCAGCACAATCGATGGTGTTACCAATTCATGGAAAAGGTCTGCGTCAAATCCCCAGTCGTCATGCATTGCCTGCTCTGCCGCTTCACG +CAGTGCCTGAGAGTTAATTTCGCTCACTTCGAACCTCTCTGTTTACTGATAAGTTCCAGATCCTCCTGGCAACTTGCACAAGTCCGACAACCCTGAACGA +CCAGGCGTCTTCGTTCATCTATCGGATCGCCACACTCACAACAATGAGTGGCAGATATAGCCTGGTGGTTCAGGCGGCGCATTTTTATTGCTGTGTTGCG +CTGTAATTCTTCTATTTCTGATGCTGAATCAATGATGTCTGCCATCTTTCATTAATCCCTGAACTGTTGGTTAATACGCATGAGGGTGAATGCGAATAAT +AAAGCTTGGCACTGGCCGTCGTTTTACAACGTCGTGACTGGGAAAACCCTGGCGTTACCCAACTTAATCGCCTTGCAGCACATCCCCCTTTCGCCAGCTG +GCGTAATAGCGAAGAGGCCCGCACCGATCGCCCTTCCCAACAGTTGCGCAGCCTGAATGGCGAATGGCGCTTTGCCTGGTTTCCGGCACCAGAAGCGGTG +CCGGAAAGCTGGCTGGAGTGCGATCTTCCTGAGGCCGATACTGTCGTCGTCCCCTCAAACTGGCAGATGCACGGTTACGATGCGCCCATCTACACCAACG +TGACCTATCCCATTACGGTCAATCCGCCGTTTGTTCCCACGGAGAATCCGACGGGTTGTTACTCGCTCACATTTAATGTTGATGAAAGCTGGCTACAGGA +AGGCCAGACGCGAATTATTTTTGATGGCGTTCCTATTGGTTAAAAAATGAGCTGATTTAACAAAAATTTAATGCGAATTTTAACAAAATATTAACGTTTA +CAATTTAAATATTTGCTTATACAATCTTCCTGTTTTTGGGGCTTTTCTGATTATCAACCGGGGTACATATGATTGACATGCTAGTTTTACGATTACCGTT +CATCGATTCTCTTGTTTGCTCCAGACTCTCAGGCAATGACCTGATAGCCTTTGTAGATCTCTCAAAAATAGCTACCCTCTCCGGCATTAATTTATCAGCT +AGAACGGTTGAATATCATATTGATGGTGATTTGACTGTCTCCGGCCTTTCTCACCCTTTTGAATCTTTACCTACACATTACTCAGGCATTGCATTTAAAA +TATATGAGGGTTCTAAAAATTTTTATCCTTGCGTTGAAATAAAGGCTTCTCCCGCAAAAGTATTACAGGGTCATAATGTTTTTGGTACAACCGATTTAGC +TTTATGCTCTGAGGCTTTATTGCTTAATTTTGCTAATTCTTTGCCTTGCCTGTATGATTTATTGGATGTTAATGCTACTACTATTAGTAGAATTGATGCC +ACCTTTTCAGCTCGCGCCCCAAATGAAAATATAGCTAAACAGGTTATTGACCATTTGCGAAATGTATCTAATGGTCAAACTAAATCTACTCGTTCGCAGA +ATTGGGAATCAACTGTTATATGGAATGAAACTTCCAGACACCGTACTTTAGTTGCATATTTAAAACATGTTGAGCTACAGCATTATATTCAGCAATTAAG +CTCTAAGCCATCCGCAAAAATGACCTCTTATCAAAAGGAGCAATTAAAGGTACTCTCTAATCCTGACCTGTTGGAGTTTGCTTCCGGTCTGGTTCGCTTT +GAAGCTCGAATTAAAACGCGATATTTGAAGTCTTTCGGGCTTCCTCTTAATCTTTTTGATGCAATCCGCTTTGCTTCTGACTATAATAGTCAGGGTAAAG +ACCTGATTTTTGATTTATGGTCATTCTCGTTTTCTGAACTGTTTAAAGCATTTGAGGGGGATTCAATGAATATTTATGACGATTCCGCAGTATTGGACGC +TATCCAGTCTAAACATTTTACTATTACCCCCTCTGGCAAAACTTCTTTTGCAAAAGCCTCTCGCTATTTTGGTTTTTATCGTCGTCTGGTAAACGAGGGT +TATGATAGTGTTGCTCTTACTATGCCTCGTAATTCCTTTTGGCGTTATGTATCTGCATTAGTTGAATGTGGTATTCCTAAATCTCAACTGATGAATCTTT +CTACCTGTAATAATGTTGTTCCGTTAGTTCGTTTTATTAACGTAGATTTTTCTTCCCAACGTCCTGACTGGTATAATGAGCCAGTTCTTAAAATCGCATA +AGGTAATTCACAATGATTAAAGTTGAAATTAAACCATCTCAAGCCCAATTTACTACTCGTTCTGGTGTTTCTCGTCAGGGCAAGCCTTATTCACTGAATG +AGCAGCTTTGTTACGTTGATTTGGGTAATGAATATCCGGTTCTTGTCAAGATTACTCTTGATGAAGGTCAGCCAGCCTATGCGCCTGGTCTGTACACCGT +TCATCTGTCCTCTTTCAAAGTTGGTCAGTTCGGTTCCCTTATGATTGACCGTCTGCGCCTCGTTCCGGCTAAGTAACATGGAGCAGGTCGCGGATTTCGA +CACAATTTATCAGGCGATGATACAAATCTCCGTTGTACTTTGTTTCGCGCTTGGTATAATCGCTGGGGGTCAAAGATGAGTGTTTTAGTGTATTCTTTTG +CCTCTTTCGTTTTAGGTTGGTGCCTTCGTAGTGGCATTACGTATTTTACCCGTTTAATGGAAACTTCCTCATGAAAAAGTCTTTAGTCCTCAAAGCCTCT +GTAGCCGTTGCTACCCTCGTTCCGATGCTGTCTTTCGCTGCTGAGGGTGACGATCCCGCAAAAGCGGCCTTTAACTCCCTGCAAGCCTCAGCGACCGAAT +ATATCGGTTATGCGTGGGCGATGGTTGTTGTCATTGTCGGCGCAACTATCGGTATCAAGCTGTTTAAGAAATTCACCTCGAAAGCAAGCTGATAAACCGA +TACAATTAAAGGCTCCTTTTGGAGCCTTTTTTTTGGAGATTTTCAACGTGAAAAAATTATTATTCGCAATTCCTTTAGTTGTTCCTTTCTATTCTCACTC +CGCTGAAACTGTTGAAAGTTGTTTAGCAAAATCCCATACAGAAAATTCATTTACTAACGTCTGGAAAGACGACAAAACTTTAGATCGTTACGCTAACTAT +GAGGGCTGTCTGTGGAATGCTACAGGCGTTGTAGTTTGTACTGGTGACGAAACTCAGTGTTACGGTACATGGGTTCCTATTGGGCTTGCTATCCCTGAAA +ATGAGGGTGGTGGCTCTGAGGGTGGCGGTTCTGAGGGTGGCGGTTCTGAGGGTGGCGGTACTAAACCTCCTGAGTACGGTGATACACCTATTCCGGGCTA +TACTTATATCAACCCTCTCGACGGCACTTATCCGCCTGGTACTGAGCAAAACCCCGCTAATCCTAATCCTTCTCTTGAGGAGTCTCAGCCTCTTAATACT +TTCATGTTTCAGAATAATAGGTTCCGAAATAGGCAGGGGGCATTAACTGTTTATACGGGCACTGTTACTCAAGGCACTGACCCCGTTAAAACTTATTACC +AGTACACTCCTGTATCATCAAAAGCCATGTATGACGCTTACTGGAACGGTAAATTCAGAGACTGCGCTTTCCATTCTGGCTTTAATGAGGATTTATTTGT +TTGTGAATATCAAGGCCAATCGTCTGACCTGCCTCAACCTCCTGTCAATGCTGGCGGCGGCTCTGGTGGTGGTTCTGGTGGCGGCTCTGAGGGTGGTGGC +TCTGAGGGTGGCGGTTCTGAGGGTGGCGGCTCTGAGGGAGGCGGTTCCGGTGGTGGCTCTGGTTCCGGTGATTTTGATTATGAAAAGATGGCAAACGCTA +ATAAGGGGGCTATGACCGAAAATGCCGATGAAAACGCGCTACAGTCTGACGCTAAAGGCAAACTTGATTCTGTCGCTACTGATTACGGTGCTGCTATCGA +TGGTTTCATTGGTGACGTTTCCGGCCTTGCTAATGGTAATGGTGCTACTGGTGATTTTGCTGGCTCTAATTCCCAAATGGCTCAAGTCGGTGACGGTGAT +AATTCACCTTTAATGAATAATTTCCGTCAATATTTACCTTCCCTCCCTCAATCGGTTGAATGTCGCCCTTTTGTCTTTGGCGCTGGTAAACCATATGAAT +TTTCTATTGATTGTGACAAAATAAACTTATTCCGTGGTGTCTTTGCGTTTCTTTTATATGTTGCCACCTTTATGTATGTATTTTCTACGTTTGCTAACAT +ACTGCGTAATAAGGAGTCTTAATCATGCCAGTTCTTTTGGGTATTCCGTTATTATTGCGTTTCCTCGGTTTCCTTCTGGTAACTTTGTTCGGCTATCTGC +TTACTTTTCTTAAAAAGGGCTTCGGTAAGATAGCTATTGCTATTTCATTGTTTCTTGCTCTTATTATTGGGCTTAACTCAATTCTTGTGGGTTATCTCTC +TGATATTAGCGCTCAATTACCCTCTGACTTTGTTCAGGGTGTTCAGTTAATTCTCCCGTCTAATGCGCTTCCCTGTTTTTATGTTATTCTCTCTGTAAAG +GCTGCTATTTTCATTTTTGACGTTAAACAAAAAATCGTTTCTTATTTGGATTGGGATAAATAATATGGCTGTTTATTTTGTAACTGGCAAATTAGGCTCT +GGAAAGACGCTCGTTAGCGTTGGTAAGATTCAGGATAAAATTGTAGCTGGGTGCAAAATAGCAACTAATCTTGATTTAAGGCTTCAAAACCTCCCGCAAG +TCGGGAGGTTCGCTAAAACGCCTCGCGTTCTTAGAATACCGGATAAGCCTTCTATATCTGATTTGCTTGCTATTGGGCGCGGTAATGATTCCTACGATGA +AAATAAAAACGGCTTGCTTGTTCTCGATGAGTGCGGTACTTGGTTTAATACCCGTTCTTGGAATGATAAGGAAAGACAGCCGATTATTGATTGGTTTCTA +CATGCTCGTAAATTAGGATGGGATATTATTTTTCTTGTTCAGGACTTATCTATTGTTGATAAACAGGCGCGTTCTGCATTAGCTGAACATGTTGTTTATT +GTCGTCGTCTGGACAGAATTACTTTACCTTTTGTCGGTACTTTATATTCTCTTATTACTGGCTCGAAAATGCCTCTGCCTAAATTACATGTTGGCGTTGT +TAAATATGGCGATTCTCAATTAAGCCCTACTGTTGAGCGTTGGCTTTATACTGGTAAGAATTTGTATAACGCATATGATACTAAACAGGCTTTTTCTAGT +AATTATGATTCCGGTGTTTATTCTTATTTAACGCCTTATTTATCACACGGTCGGTATTTCAAACCATTAAATTTAGGTCAGAAGATGAAATTAACTAAAA +TATATTTGAAAAAGTTTTCTCGCGTTCTTTGTCTTGCGATTGGATTTGCATCAGCATTTACATATAGTTATATAACCCAACCTAAGCCGGAGGTTAAAAA +GGTAGTCTCTCAGACCTATGATTTTGATAAATTCACTATTGACTCTTCTCAGCGTCTTAATCTAAGCTATCGCTATGTTTTCAAGGATTCTAAGGGAAAA +TTAATTAATAGCGACGATTTACAGAAGCAAGGTTATTCACTCACATATATTGATTTATGTACTGTTTCCATTAAAAAAGGTAATTCAAATGAAATTGTTA +AATGTAATTAATTTTGTTTTCTTGATGTTTGTTTCATCATCTTCTTTTGCTCAGGTAATTGAAATGAATAATTCGCCTCTGCGCGATTTTGTAACTTGGT +ATTCAAAGCAATCAGGCGAATCCGTTATTGTTTCTCCCGATGTAAAAGGTACTGTTACTGTATATTCATCTGACGTTAAACCTGAAAATCTACGCAATTT +CTTTATTTCTGTTTTACGTGCAAATAATTTTGATATGGTAGGTTCTAACCCTTCCATTATTCAGAAGTATAATCCAAACAATCAGGATTATATTGATGAA +TTGCCATCATCTGATAATCAGGAATATGATGATAATTCCGCTCCTTCTGGTGGTTTCTTTGTTCCGCAAAATGATAATGTTACTCAAACTTTTAAAATTA +ATAACGTTCGGGCAAAGGATTTAATACGAGTTGTCGAATTGTTTGTAAAGTCTAATACTTCTAAATCCTCAAATGTATTATCTATTGACGGCTCTAATCT +ATTAGTTGTTAGTGCTCCTAAAGATATTTTAGATAACCTTCCTCAATTCCTTTCAACTGTTGATTTGCCAACTGACCAGATATTGATTGAGGGTTTGATA +TTTGAGGTTCAGCAAGGTGATGCTTTAGATTTTTCATTTGCTGCTGGCTCTCAGCGTGGCACTGTTGCAGGCGGTGTTAATACTGACCGCCTCACCTCTG +TTTTATCTTCTGCTGGTGGTTCGTTCGGTATTTTTAATGGCGATGTTTTAGGGCTATCAGTTCGCGCATTAAAGACTAATAGCCATTCAAAAATATTGTC +TGTGCCACGTATTCTTACGCTTTCAGGTCAGAAGGGTTCTATCTCTGTTGGCCAGAATGTCCCTTTTATTACTGGTCGTGTGACTGGTGAATCTGCCAAT +GTAAATAATCCATTTCAGACGATTGAGCGTCAAAATGTAGGTATTTCCATGAGCGTTTTTCCTGTTGCAATGGCTGGCGGTAATATTGTTCTGGATATTA +CCAGCAAGGCCGATAGTTTGAGTTCTTCTACTCAGGCAAGTGATGTTATTACTAATCAAAGAAGTATTGCTACAACGGTTAATTTGCGTGATGGACAGAC +TCTTTTACTCGGTGGCCTCACTGATTATAAAAACACTTCTCAGGATTCTGGCGTACCGTTCCTGTCTAAAATCCCTTTAATCGGCCTCCTGTTTAGCTCC +CGCTCTGATTCTAACGAGGAAAGCACGTTATACGTGCTCGTCAAAGCAACCATAGTACGCGCCCTGTAGCGGCGCATTAAGCGCGGCGGGTGTGGTGGTT +ACGCGCAGCGTGACCGCTACACTTGCCAGCGCCCTAGCGCCCGCTCCTTTCGCTTTCTTCCCTTCCTTTCTCGCCACGTTCGCCGGCTTTCCCCGTCAAG +CTCTAAATCGGGGGCTCCCTTTAGGGTTCCGATTTAGTGCTTTACGGCACCTCGACCCCAAAAAACTTGATTTGGGTGATGGTTCACGTAGTGGGCCATC +GCCCTGATAGACGGTTTTTCGCCCTTTGACGTTG +''') + _m13_variants = { M13Variant.p7249: _7249, M13Variant.p7560: _7560, M13Variant.p8064: _8064, + M13Variant.p8634: _8634, } ################## @@ -927,7 +1023,7 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> D @staticmethod def from_json( - json_map: Dict[str, Any]) -> 'Modification': # remove quotes when Py3.6 support dropped + json_map: Dict[str, Any]) -> Modification: location = json_map[mod_location_key] if location == "5'": return Modification5Prime.from_json(json_map) @@ -965,9 +1061,8 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> D ret[mod_location_key] = "5'" return ret - # remove quotes when Py3.6 support dropped @staticmethod - def from_json(json_map: Dict[str, Any]) -> 'Modification5Prime': + def from_json(json_map: Dict[str, Any]) -> Modification5Prime: display_text = json_map[mod_display_text_key] location = json_map[mod_location_key] assert location == "5'" @@ -1002,9 +1097,8 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> D ret[mod_location_key] = "3'" return ret - # remove quotes when Py3.6 support dropped @staticmethod - def from_json(json_map: Dict[str, Any]) -> 'Modification3Prime': + def from_json(json_map: Dict[str, Any]) -> Modification3Prime: display_text = json_map[mod_display_text_key] location = json_map[mod_location_key] assert location == "3'" @@ -1042,9 +1136,8 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> D list(self.allowed_bases)) if suppress_indent else list(self.allowed_bases) return ret - # remove quotes when Py3.6 support dropped @staticmethod - def from_json(json_map: Dict[str, Any]) -> 'ModificationInternal': + def from_json(json_map: Dict[str, Any]) -> ModificationInternal: display_text = json_map[mod_display_text_key] location = json_map[mod_location_key] assert location == "internal" @@ -1090,7 +1183,7 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> D return dct @staticmethod - def from_json(json_map: Dict[str, Any]) -> 'Position3D': # remove quotes when Py3.6 support dropped + def from_json(json_map: Dict[str, Any]) -> Position3D: if position_origin_key in json_map: origin_ = json_map[position_origin_key] x = origin_[position_x_key] @@ -1130,7 +1223,7 @@ class HelixGroup(_JSONSerializable): A :any:`HelixGroup` is useful for grouping together helices that should all be in parallel, as part of a design where different groups are not parallel. In particular, each :any:`HelixGroup` can be given its own 3D position and pitch/yaw/roll orientation angles. Each :any:`HelixGroup` does - not actually *contain* its helices; they are associated through the field `Helix.group`, which is + not actually *contain* its helices; they are associated through the field :data:`Helix.group`, which is a string representing a key in the dict ``groups`` specified in the constructor for :any:`Design`. If there are :any:`HelixGroup`'s explicitly specified, then the field :py:data:`Design.grid` is ignored. @@ -1157,7 +1250,7 @@ class HelixGroup(_JSONSerializable): Rotation is *clockwise* in the main view, i.e., clockwise in the Y-Z plane, around the X-axis, when Y-axis points down, Z-axis points right, and X-axis points out of the page. - See https://en.wikipedia.org/wiki/Aircraft_principal_axes + See https://en.wikipedia.org/wiki/Aircraft_principal_axes. Units are degrees.""" roll: float = 0 @@ -1174,7 +1267,7 @@ class HelixGroup(_JSONSerializable): Rotation is *clockwise* while looking down onto the main view, i.e., in the X-Z plane, around the Y-axis, when X-axis points down, Z-axis points right, and Y-axis points into the page. - See https://en.wikipedia.org/wiki/Aircraft_principal_axes + See https://en.wikipedia.org/wiki/Aircraft_principal_axes. Units are degrees.""" helices_view_order: Optional[List[int]] = None @@ -1184,6 +1277,11 @@ class HelixGroup(_JSONSerializable): grid: Grid = Grid.none """:any:`Grid` of this :any:`HelixGroup` used to interpret the field :data:`Helix.grid_position`.""" + def has_default_position_and_orientation(self): + # we don't bother checking grid or helices_view_order because those are written to top-level + # fields of Design if the group is otherwise default + return self.position == origin and self.pitch == self.roll == self.yaw == 0 + def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> Dict[str, Any]: dct: Dict[str, Any] = dict() @@ -1216,7 +1314,7 @@ def _assign_default_helices_view_order(self, helices_in_group: Dict[int, 'Helix' self.helices_view_order = _check_helices_view_order_and_return(self.helices_view_order, helix_idxs) @staticmethod - def from_json(json_map: dict, **kwargs: Any) -> 'HelixGroup': # remove quotes when Py3.6 support dropped + def from_json(json_map: dict, **kwargs: Any) -> HelixGroup: grid: Grid = optional_field(Grid.none, json_map, grid_key, transformer=Grid) # grid: Grid = Grid.none # if grid_key in json_map: @@ -1273,7 +1371,22 @@ class Geometry(_JSONSerializable): """Minor groove angle in degrees.""" inter_helix_gap: float = 1.0 - """Gap between helices in nanometers (due to electrostatic repulsion; needed to display to scale).""" + """ + Gap between helices in nanometers due to electrostatic repulsion. This is used by the scadnano + web interface to display an appropriate aspect ratio for 2D DNA structures. + + The default value of 1.0 nm is approximately the average distance, as measured by atomic force + microscopy (AFM) images, for 2D DNA origami using the :data:`Grid.square` grid, + with 32 base pairs in between consecutive crossovers between two helices. Such a structure with `n` + parallel helices generally is measured to be about 3`n` nm high on AFM images. Since each DNA helix + is 2 nm diameter, this implies an average inter-helix gap of 1.0 nm, though of course it is just an + average, and the actual gap varies depending on distance to the nearest crossover: at a crossover + the distance is close to 0 and halfway between two crossovers, the distance is greater than 1 nm. + + This value may be inappropriate for designs with different crossover spacing, for example + single-stranded tiles with 21 base pairs between consecutive crossovers. (In that case 0.5 nm seems + to be a more appropriate approximation.) + """ def distance_between_helices(self) -> float: return 2 * self.helix_radius + self.inter_helix_gap @@ -1282,7 +1395,7 @@ def is_default(self) -> bool: return self == _default_geometry @staticmethod - def from_json(json_map: dict) -> 'Geometry': # remove quotes when Py3.6 support dropped + def from_json(json_map: dict) -> Geometry: geometry = Geometry() geometry.rise_per_base_pair = optional_field(_default_geometry.rise_per_base_pair, json_map, rise_per_base_pair_key, @@ -1334,10 +1447,11 @@ class Helix(_JSONSerializable): :py:data:`Domain.forward` = ``true`` and the other must have :py:data:`Domain.forward` = ``false``. - Once part of a :any:`Design`, a :any:`Helix` has an index (accessible via :py:meth:`Helix.idx` - once the :any:`Design` is created) - representing its order in the list of all :any:`Helix`'s. This index is how a :any:`Domain` is - associated to the :any:`Helix` via the integer index :any:`Domain.helix`. + Each :any:`Helix` has an index, accessible via :py:data:`Helix.idx`. By default this + is its order in the list of all :any:`Helix`'s (this is how the :any:`Design` constructor sets the + field if it is not already set), but it can be manually assigned to be any integer that is unique to the + :any:`Helix`. This index is how a :any:`Domain` is + associated to the :any:`Helix` via the field :any:`Domain.helix`. """ max_offset: Optional[int] = None # type: ignore @@ -1437,7 +1551,7 @@ class Helix(_JSONSerializable): """Name of the :any:`HelixGroup` to which this :any:`Helix` belongs.""" # for optimization; list of domains on that Helix - _domains: List['Domain'] = field(default_factory=list) + _domains: List[Domain] = field(default_factory=list) def __post_init__(self) -> None: if self.major_tick_start is None: # type: ignore @@ -1504,7 +1618,7 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> D return NoIndent(dct) if suppress_indent and use_no_indent_helix else dct @staticmethod - def from_json(json_map: dict) -> 'Helix': # remove quotes when Py3.6 support dropped + def from_json(json_map: dict) -> Helix: grid_position: Optional[Tuple[int, int]] = None if grid_position_key in json_map: gp_list = json_map[grid_position_key] @@ -1579,7 +1693,7 @@ def calculate_major_ticks(self, grid: Grid) -> List[int]: return list(range(self.major_tick_start, self.max_offset + 1, distance)) @property - def domains(self) -> List['Domain']: + def domains(self) -> List[Domain]: """ Return :any:`Domain`'s on this :any:`Helix`. Assigned when a :any:`Design` is created using this :any:`Helix`. @@ -1612,6 +1726,8 @@ def backbone_angle_at_offset(self, offset: int, forward: bool, geometry: Geometr offset on this helix :param forward: whether to compute angle for the forward or reverse strand + :param geometry: + :any:`Geometry` parameters to determine bases per turn :return: backbone angle at *offset* for the strand in the direction given by *forward*. """ @@ -1626,12 +1742,8 @@ def _is_close(x1: float, x2: float) -> bool: return abs(x1 - x2) < _floating_point_tolerance -DomainLabel = TypeVar('DomainLabel') -StrandLabel = TypeVar('StrandLabel') - - @dataclass -class Domain(_JSONSerializable, Generic[DomainLabel]): +class Domain(_JSONSerializable): """ A maximal portion of a :any:`Strand` that is continguous on a single :any:`Helix`. A :any:`Strand` contains a list of :any:`Domain`'s (and also potentially :any:`Loopout`'s). @@ -1683,15 +1795,11 @@ class Domain(_JSONSerializable, Generic[DomainLabel]): This is used to interoperate with the dsd DNA sequence design package.""" - label: Optional[DomainLabel] = None + label: Optional[str] = None """ - Generic "label" object to associate to this :any:`Domain`. + This can be used to attach a "label" to associate to this :any:`Loopout`. - Useful for associating extra information with the :any:`Domain` that will be serialized, for example, - for DNA sequence design. It must be an object (e.g., a dict or primitive type such as str or int) - that is naturally JSON serializable. (Calling - `json.dumps `_ - on the object should succeed without having to specify a custom encoder.) + See :any:`Strand.label` for examples. """ dna_sequence: Optional[str] = None @@ -1704,8 +1812,8 @@ class Domain(_JSONSerializable, Generic[DomainLabel]): """ # not serialized; for efficiency - # remove quotes when Py3.6 support dropped - _parent_strand: Optional['Strand'] = field(init=False, repr=False, compare=False, default=None) + + _parent_strand: Optional[Strand] = field(init=False, repr=False, compare=False, default=None) def __post_init__(self) -> None: self._check_start_end() @@ -1730,7 +1838,7 @@ def to_json_serializable(self, suppress_indent: bool = True, return NoIndent(dct) if suppress_indent else dct @staticmethod - def from_json(json_map: Dict[str, Any]) -> 'Domain': # remove quotes when Py3.6 support dropped + def from_json(json_map: Dict[str, Any]) -> Domain: helix = mandatory_field(Domain, json_map, helix_idx_key) forward = mandatory_field(Domain, json_map, forward_key, legacy_keys=legacy_forward_keys) start = mandatory_field(Domain, json_map, start_key) @@ -1770,9 +1878,9 @@ def __repr__(self) -> str: def __str__(self) -> str: return repr(self) if self.name is None else self.name - def strand(self) -> 'Strand': # remove quotes when Py3.6 support dropped + def strand(self) -> Strand: """ - :return: The :any:`Strand` that contains this :any:`Loopout`. + :return: The :any:`Strand` that contains this :any:`Domain`. """ if self._parent_strand is None: raise ValueError('_parent_strand has not yet been set') @@ -1782,7 +1890,7 @@ def set_name(self, name: str) -> None: """Sets name of this :any:`Domain`.""" self.name = name - def set_label(self, label: DomainLabel) -> None: + def set_label(self, label: str) -> None: """Sets label of this :any:`Domain`.""" self.label = label @@ -1966,11 +2074,7 @@ def _between_5p_and_offset(self, offset_to_test: int, offset_edge: int) -> bool: # return ((self.direction == Direction.left and self.start <= offset_to_test < offset_edge) or # (self.direction == Direction.forward and offset_edge < offset_to_test < self.end)) - # The type hint 'Domain' must be in quotes since Domain is not yet defined. - # This is a "forward reference": https://www.python.org/dev/peps/pep-0484/#forward-references - # remove quotes when Py3.6 support dropped - # def overlaps(self, other: Domain) -> bool: - def overlaps(self, other: 'Domain') -> bool: + def overlaps(self, other: Domain) -> bool: r"""Indicates if this :any:`Domain`'s set of offsets (the set :math:`\{x \in \mathbb{N} \mid` ``self.start`` @@ -1983,9 +2087,8 @@ def overlaps(self, other: 'Domain') -> bool: return (self.forward == (not other.forward) and self.compute_overlap(other)[0] >= 0) - # remove quotes when Py3.6 support dropped # def overlaps_illegally(self, other: Domain): - def overlaps_illegally(self, other: 'Domain') -> bool: + def overlaps_illegally(self, other: Domain) -> bool: r"""Indicates if this :any:`Domain`'s set of offsets (the set :math:`\{x \in \mathbb{N} \mid` ``self.start`` @@ -1998,9 +2101,7 @@ def overlaps_illegally(self, other: 'Domain') -> bool: return (self.forward == other.forward and self.compute_overlap(other)[0] >= 0) - # remove quotes when Py3.6 support dropped - # def compute_overlap(self, other: Domain) -> Tuple[int, int]: - def compute_overlap(self, other: 'Domain') -> Tuple[int, int]: + def compute_overlap(self, other: Domain) -> Tuple[int, int]: """Return [left,right) offset indicating overlap between this Domain and `other`. Return ``(-1,-1)`` if they do not overlap (different helices, or non-overlapping regions @@ -2047,7 +2148,7 @@ def is_3p_domain(self) -> bool: @dataclass -class Loopout(_JSONSerializable, Generic[DomainLabel]): +class Loopout(_JSONSerializable): """Represents a single-stranded loopout on a :any:`Strand`. One could think of a :any:`Loopout` as a type of :any:`Domain`, but none of the fields of @@ -2093,15 +2194,11 @@ class Loopout(_JSONSerializable, Generic[DomainLabel]): This is used to interoperate with the dsd DNA sequence design package. """ - label: Optional[DomainLabel] = None + label: Optional[str] = None """ - Generic "label" object to associate to this :any:`Loopout`. + This can be used to attach a "label" to associate to this :any:`Loopout`. - Useful for associating extra information with the :any:`Loopout` that will be serialized, for example, - for DNA sequence design. It must be an object (e.g., a dict or primitive type such as str or int) - that is naturally JSON serializable. (Calling - `json.dumps `_ - on the object should succeed without having to specify a custom encoder.) + See :any:`Strand.label` for examples. """ dna_sequence: Optional[str] = None @@ -2113,8 +2210,8 @@ class Loopout(_JSONSerializable, Generic[DomainLabel]): """ # not serialized; for efficiency - # remove quotes when Py3.6 support dropped - _parent_strand: Optional['Strand'] = field(init=False, repr=False, compare=False, default=None) + + _parent_strand: Optional[Strand] = field(init=False, repr=False, compare=False, default=None) def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> Union[Dict[str, Any], NoIndent]: @@ -2128,7 +2225,7 @@ def to_json_serializable(self, suppress_indent: bool = True, return NoIndent(dct) if suppress_indent else dct @staticmethod - def from_json(json_map: Dict[str, Any]) -> 'Loopout': # remove quotes when Py3.6 support dropped + def from_json(json_map: Dict[str, Any]) -> Loopout: # XXX: this should never fail since we detect whether to call this from_json by the presence # of a length key in json_map length_str = mandatory_field(Loopout, json_map, loopout_key) @@ -2139,7 +2236,7 @@ def from_json(json_map: Dict[str, Any]) -> 'Loopout': # remove quotes when Py3. color = Color.from_json(color_json) return Loopout(length=length, name=name, label=label, color=color) - def strand(self) -> 'Strand': # remove quotes when Py3.6 support dropped + def strand(self) -> Strand: """ :return: The :any:`Strand` that contains this :any:`Loopout`. """ @@ -2149,10 +2246,10 @@ def strand(self) -> 'Strand': # remove quotes when Py3.6 support dropped def __repr__(self) -> str: return f'Loopout(' + \ - (f'{self.name}, ' if self.name is not None else '') + \ - f'{self.length}, ' + \ - (f'{self.label}, ' if self.label is not None else '') + \ - f')' + (f'{self.name}, ' if self.name is not None else '') + \ + f'{self.length}, ' + \ + (f'{self.label}, ' if self.label is not None else '') + \ + f')' def __str__(self) -> str: return repr(self) if self.name is None else self.name @@ -2161,7 +2258,7 @@ def set_name(self, name: str) -> None: """Sets name of this :any:`Loopout`.""" self.name = name - def set_label(self, label: Optional[DomainLabel]) -> None: + def set_label(self, label: Optional[str]) -> None: """Sets label of this :any:`Loopout`.""" self.label = label @@ -2193,8 +2290,9 @@ def get_seq_start_idx(self) -> int: @dataclass -class Extension(_JSONSerializable, Generic[DomainLabel]): - """Represents a single-stranded extension on either the 3' or 5' +class Extension(_JSONSerializable): + # using raw string literal for docstring to avoid warning about backslash + r"""Represents a single-stranded extension on either the 3' or 5' end of :any:`Strand`. One could think of an :any:`Extension` as a type of :any:`Domain`, but none of the fields of @@ -2252,15 +2350,12 @@ class Extension(_JSONSerializable, Generic[DomainLabel]): for degrees strictly between 0 and 180. """ - label: Optional[DomainLabel] = None + label: Optional[str] = None + """ - Generic "label" object to associate to this :any:`Extension`. + This can be used to attach a "label" to associate to this :any:`Extension`. - Useful for associating extra information with the :any:`Extension` that will be serialized, for example, - for DNA sequence design. It must be an object (e.g., a dict or primitive type such as str or int) - that is naturally JSON serializable. (Calling - `json.dumps `_ - on the object should succeed without having to specify a custom encoder.) + See :any:`Strand.label` for examples. """ name: Optional[str] = None @@ -2277,8 +2372,8 @@ class Extension(_JSONSerializable, Generic[DomainLabel]): """ # not serialized; for efficiency - # remove quotes when Py3.6 support dropped - _parent_strand: Optional['Strand'] = field(init=False, repr=False, compare=False, default=None) + + _parent_strand: Optional[Strand] = field(init=False, repr=False, compare=False, default=None) def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) \ -> Union[Dict[str, Any], NoIndent]: @@ -2295,7 +2390,7 @@ def dna_length(self) -> int: """Length of this :any:`Extension`; same as field :data:`Extension.num_bases`.""" return self.num_bases - def set_label(self, label: Optional[DomainLabel]) -> None: + def set_label(self, label: Optional[str]) -> None: """Sets label of this :any:`Extension`.""" self.label = label @@ -2435,7 +2530,7 @@ def _check_idt_string_not_none_or_empty(value: str, field_name: str) -> None: raise IllegalDesignError(f'field {field_name} in IDTFields cannot be empty') -class StrandBuilder(Generic[StrandLabel, DomainLabel]): +class StrandBuilder: """ Represents a :any:`Strand` that is being built in an existing :any:`Design`. @@ -2461,25 +2556,23 @@ class StrandBuilder(Generic[StrandLabel, DomainLabel]): strand_builder.as_scaffold() """ - # remove quotes when Py3.6 support dropped - def __init__(self, design: 'Design[StrandLabel, DomainLabel]', helix: int, offset: int): - self.design: Design[StrandLabel, DomainLabel] = design + def __init__(self, design: Design, helix: int, offset: int): + self.design: Design = design self.current_helix: int = helix self.current_offset: int = offset # self.loopout_length: Optional[int] = None - self._strand: Optional[Strand[StrandLabel, DomainLabel]] = None + self._strand: Optional[Strand] = None self.just_moved_to_helix: bool = True - self.last_domain: Optional[Domain[DomainLabel]] = None + self.last_domain: Optional[Domain] = None @property - def strand(self) -> 'Strand[StrandLabel, DomainLabel]': + def strand(self) -> Strand: if self._strand is None: raise ValueError('no Strand created yet; make at least one domain first') return self._strand - # remove quotes when Py3.6 support dropped def cross(self, helix: int, offset: Optional[int] = None, move: Optional[int] = None) \ - -> 'StrandBuilder[StrandLabel, DomainLabel]': + -> StrandBuilder: """ Add crossover. To have any effect, must be followed by call to :py:meth:`StrandBuilder.to` or :py:meth:`StrandBuilder.move`. @@ -2516,9 +2609,8 @@ def _most_recently_added_substrand_is_instance_of_class(self, cls: Type) -> bool def _most_recently_added_substrand_is_extension(self): return self._most_recently_added_substrand_is_instance_of_class(Extension) - # remove quotes when Py3.6 support dropped def loopout(self, helix: int, length: int, offset: Optional[int] = None, move: Optional[int] = None) \ - -> 'StrandBuilder[StrandLabel, DomainLabel]': + -> StrandBuilder: """ Like :py:meth:`StrandBuilder.cross`, but creates a :any:`Loopout` instead of a crossover. @@ -2543,7 +2635,7 @@ def extension_3p(self, num_bases: int, display_length: float = default_display_length, display_angle: float = default_display_angle - ) -> 'StrandBuilder[StrandLabel, DomainLabel]': + ) -> StrandBuilder: """ Creates an :any:`Extension` after verifying that it is valid to add an :any:`Extension` to the :any:`Strand` as a 3' :any:`Extension`. @@ -2582,7 +2674,7 @@ def extension_5p(self, num_bases: int, display_length: float = default_display_length, display_angle: float = default_display_angle - ) -> 'StrandBuilder[StrandLabel, DomainLabel]': + ) -> StrandBuilder: """ Creates an :any:`Extension` after verifying that it is valid to add an :any:`Extension` to the :any:`Strand` as a 5' :any:`Extension`. @@ -2606,8 +2698,7 @@ def _verify_extension_5p_is_valid(self): 'Cannot add a 5\' extension when there are already domains. ' 'Did you mean to create a 3\' extension?') - # remove quotes when Py3.6 support dropped - def move(self, delta: int) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def move(self, delta: int) -> StrandBuilder: """ Extends this :any:`StrandBuilder` on the current helix to offset given by the current offset plus `delta`, which adds a new :any:`Domain` to the :any:`Strand` being built. This is a @@ -2629,8 +2720,7 @@ def move(self, delta: int) -> 'StrandBuilder[StrandLabel, DomainLabel]': """ return self.to(self.current_offset + delta) - # remove quotes when Py3.6 support dropped - def to(self, offset: int) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def to(self, offset: int) -> StrandBuilder: """ Extends this :any:`StrandBuilder` on the current helix to offset `offset`, which adds a new :any:`Domain` to the :any:`Strand` being built. This is an @@ -2672,7 +2762,7 @@ def to(self, offset: int) -> 'StrandBuilder[StrandLabel, DomainLabel]': else: raise IllegalDesignError(f'offset {offset} cannot be equal to current offset') - domain: Domain[DomainLabel] = Domain(helix=self.current_helix, forward=forward, start=start, end=end) + domain: Domain = Domain(helix=self.current_helix, forward=forward, start=start, end=end) self.last_domain = domain if self._strand is not None: self.design.append_domain(self._strand, domain) @@ -2680,7 +2770,7 @@ def to(self, offset: int) -> 'StrandBuilder[StrandLabel, DomainLabel]': self._strand = Strand(domains=[domain]) self.design.add_strand(self._strand) - self.design._check_strand_has_legal_offsets_in_helices(self._strand) + self.design._check_strand_has_legal_offsets_in_helices(self._strand) # noqa self.current_offset = offset @@ -2691,8 +2781,7 @@ def _most_recently_added_substrand_is_extension_3p(self) -> bool: return False return len(self._strand.domains) > 1 and self._most_recently_added_substrand_is_extension() - # remove quotes when Py3.6 support dropped - def update_to(self, offset: int) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def update_to(self, offset: int) -> StrandBuilder: """ Like :py:meth:`StrandBuilder.to`, but changes the current offset without creating a new :any:`Domain`. So unlike :py:meth:`StrandBuilder.to`, several consecutive calls to @@ -2728,7 +2817,7 @@ def update_to(self, offset: int) -> 'StrandBuilder[StrandLabel, DomainLabel]': return self - def as_circular(self) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def as_circular(self) -> StrandBuilder: """ Makes :any:`Strand` being built circular. @@ -2739,8 +2828,7 @@ def as_circular(self) -> 'StrandBuilder[StrandLabel, DomainLabel]': self._strand.set_circular() return self - # remove quotes when Py3.6 support dropped - def as_scaffold(self) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def as_scaffold(self) -> StrandBuilder: """ Makes :any:`Strand` being built a scaffold. @@ -2754,7 +2842,7 @@ def as_scaffold(self) -> 'StrandBuilder[StrandLabel, DomainLabel]': def with_idt(self, scale: str = default_idt_scale, purification: str = default_idt_purification, plate: Optional[str] = None, well: Optional[str] = None) \ - -> 'StrandBuilder[StrandLabel, DomainLabel]': + -> StrandBuilder: """ Gives :any:`IDTFields` value to :any:`Strand` being built. Only a name is required; other fields are given reasonable default values. @@ -2775,8 +2863,7 @@ def with_idt(self, scale: str = default_idt_scale, plate=plate, well=well) return self - # remove quotes when Py3.6 support dropped - def with_modification_5p(self, mod: Modification5Prime) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def with_modification_5p(self, mod: Modification5Prime) -> StrandBuilder: """ Sets Strand being built to have given 5' modification. @@ -2788,8 +2875,7 @@ def with_modification_5p(self, mod: Modification5Prime) -> 'StrandBuilder[Strand self._strand.set_modification_5p(mod) return self - # remove quotes when Py3.6 support dropped - def with_modification_3p(self, mod: Modification3Prime) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def with_modification_3p(self, mod: Modification3Prime) -> StrandBuilder: """ Sets Strand being built to have given 3' modification. @@ -2801,9 +2887,8 @@ def with_modification_3p(self, mod: Modification3Prime) -> 'StrandBuilder[Strand self._strand.set_modification_3p(mod) return self - # remove quotes when Py3.6 support dropped def with_modification_internal(self, idx: int, mod: ModificationInternal, warn_on_no_dna: bool) \ - -> 'StrandBuilder[StrandLabel, DomainLabel]': + -> StrandBuilder: """ Sets Strand being built to have given internal modification. @@ -2817,8 +2902,7 @@ def with_modification_internal(self, idx: int, mod: ModificationInternal, warn_o self._strand.set_modification_internal(idx, mod, warn_on_no_dna) return self - # remove quotes when Py3.6 support dropped - def with_color(self, color: Color) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def with_color(self, color: Color) -> StrandBuilder: """ Sets Strand being built to have given color. @@ -2830,9 +2914,8 @@ def with_color(self, color: Color) -> 'StrandBuilder[StrandLabel, DomainLabel]': self._strand.set_color(color) return self - # remove quotes when Py3.6 support dropped def with_sequence(self, sequence: str, assign_complement: bool = False) \ - -> 'StrandBuilder[StrandLabel, DomainLabel]': + -> StrandBuilder: """ Assigns `sequence` as DNA sequence of the :any:`Strand` being built. This should be done after the :any:`Strand`'s structure is done being built, e.g., @@ -2852,9 +2935,8 @@ def with_sequence(self, sequence: str, assign_complement: bool = False) \ self.design.assign_dna(strand=self._strand, sequence=sequence, assign_complement=assign_complement) return self - # remove quotes when Py3.6 support dropped def with_domain_sequence(self, sequence: str, assign_complement: bool = False) \ - -> 'StrandBuilder[StrandLabel, DomainLabel]': + -> StrandBuilder: """ Assigns `sequence` as DNA sequence of the most recently created :any:`Domain` in the :any:`Strand` being built. This should be called immediately after a :any:`Domain` is created @@ -2890,8 +2972,7 @@ def with_domain_sequence(self, sequence: str, assign_complement: bool = False) \ assign_complement=assign_complement) return self - # remove quotes when Py3.6 support dropped - def with_domain_color(self, color: Color) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def with_domain_color(self, color: Color) -> StrandBuilder: """ Sets most recent :any:`Domain`/:any:`Loopout`/:any:`Extension` to have given color. @@ -2907,8 +2988,7 @@ def with_domain_color(self, color: Color) -> 'StrandBuilder[StrandLabel, DomainL last_domain.color = color return self - # remove quotes when Py3.6 support dropped - def with_name(self, name: str) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def with_name(self, name: str) -> StrandBuilder: """ Assigns `name` as name of the :any:`Strand` being built. @@ -2924,8 +3004,7 @@ def with_name(self, name: str) -> 'StrandBuilder[StrandLabel, DomainLabel]': self._strand.set_name(name) return self - # remove quotes when Py3.6 support dropped - def with_label(self, label: StrandLabel) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def with_label(self, label: str) -> StrandBuilder: """ Assigns `label` as label of the :any:`Strand` being built. @@ -2941,8 +3020,7 @@ def with_label(self, label: StrandLabel) -> 'StrandBuilder[StrandLabel, DomainLa self._strand.set_label(label) return self - # remove quotes when Py3.6 support dropped - def with_domain_name(self, name: str) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def with_domain_name(self, name: str) -> StrandBuilder: """ Assigns `name` as of the most recently created :any:`Domain` or :any:`Loopout` in the :any:`Strand` being built. This should be called immediately after a :any:`Domain` is created @@ -2966,8 +3044,7 @@ def with_domain_name(self, name: str) -> 'StrandBuilder[StrandLabel, DomainLabel last_domain.set_name(name) return self - # remove quotes when Py3.6 support dropped - def with_domain_label(self, label: DomainLabel) -> 'StrandBuilder[StrandLabel, DomainLabel]': + def with_domain_label(self, label: str) -> StrandBuilder: """ Assigns `label` as label of the most recently created :any:`Domain` or :any:`Loopout` in the :any:`Strand` being built. This should be called immediately after @@ -2997,7 +3074,7 @@ def with_domain_label(self, label: DomainLabel) -> 'StrandBuilder[StrandLabel, D return self def with_deletions(self, - deletions: Union[int, Iterable[int]]) -> 'StrandBuilder[StrandLabel, DomainLabel]': + deletions: Union[int, Iterable[int]]) -> StrandBuilder: """ Assigns `deletions` as the deletion(s) of the most recently created :any:`Domain` the :any:`Strand` being built. This should be called immediately after @@ -3042,7 +3119,7 @@ def with_deletions(self, return self def with_insertions(self, insertions: Union[Tuple[int, int], Iterable[Tuple[int, int]]]) \ - -> 'StrandBuilder[StrandLabel, DomainLabel]': + -> StrandBuilder: """ Assigns `insertions` as the insertion(s) of the most recently created :any:`Domain` the :any:`Strand` being built. This should be called immediately after @@ -3095,7 +3172,7 @@ def with_insertions(self, insertions: Union[Tuple[int, int], Iterable[Tuple[int, @dataclass -class Strand(_JSONSerializable, Generic[StrandLabel, DomainLabel]): +class Strand(_JSONSerializable): """ Represents a single strand of DNA. @@ -3132,7 +3209,7 @@ class Strand(_JSONSerializable, Generic[StrandLabel, DomainLabel]): uses for the scaffold. """ - domains: List[Union[Domain[DomainLabel], Loopout[DomainLabel], Extension[DomainLabel]]] + domains: List[Union[Domain, Loopout, Extension]] """:any:`Domain`'s (or :any:`Loopout`'s) composing this Strand. Each :any:`Domain` is contiguous on a single :any:`Helix` and could be either single-stranded or double-stranded, @@ -3212,29 +3289,44 @@ def dna_sequence(self) -> Optional[str]: This is used to interoperate with the dsd DNA sequence design package. """ - label: Optional[StrandLabel] = None - """Generic "label" object to associate to this :any:`Strand`. - - Useful for associating extra information with the Strand that will be serialized, for example, - for DNA sequence design. It must be an object (e.g., a dict or primitive type such as str or int) - that is naturally JSON serializable. (Calling ``json.dumps`` on the object should succeed without - having to specify a custom encoder.) + label: Optional[str] = None + """ + This can be used to attach a "label" to associate to this :any:`Strand`. + + Useful for associating extra information with the :any:`Strand` that will be serialized, for example, + for DNA sequence design. It can be useful to create "groups" of strands related in some way. + + Prior to version 0.18.0, this was allowed to be an arbitrary JSON-serializable object. + Now it is just a string + (see https://github.com/UC-Davis-molecular-computing/scadnano-python-package/issues/261). + To store more structured data, it is necessary to serialize (convert to a string) the data manually. + For example, if you want to store a list of numbers, you can do so as a string like this: + + .. code-block:: python + + import json + + nums = [1, 2, 3] + strand.label = json.dumps(nums) # stores strand.label as the string '[1, 2, 3]' + + # and to get the structured data back out: + nums = json.loads(strand.label) # nums is now the list [1, 2, 3] """ # not serialized; efficient way to see a list of all domains on a given helix - _helix_idx_domain_map: Dict[int, List[Domain[DomainLabel]]] = field( + _helix_idx_domain_map: Dict[int, List[Domain]] = field( init=False, repr=False, compare=False, default_factory=dict) def __init__(self, - domains: List[Union[Domain[DomainLabel], Loopout[DomainLabel], Extension[DomainLabel]]], + domains: List[Union[Domain, Loopout, Extension]], circular: bool = False, color: Optional[Color] = None, idt: Optional[IDTFields] = None, is_scaffold: bool = False, modification_5p: Optional[Modification5Prime] = None, modification_3p: Optional[Modification3Prime] = None, modifications_int: Optional[Dict[int, ModificationInternal]] = None, name: Optional[str] = None, - label: Optional[StrandLabel] = None, - _helix_idx_domain_map: Dict[int, List[Domain[DomainLabel]]] = None, + label: Optional[str] = None, + _helix_idx_domain_map: Dict[int, List[Domain]] = None, dna_sequence: Optional[str] = None): self.domains = domains self.circular = circular @@ -3287,12 +3379,12 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> D dct[modifications_int_key] = NoIndent(mods_dict) if suppress_indent else mods_dict if self.label is not None: - dct[strand_label_key] = NoIndent(self.label) if suppress_indent else self.label + dct[strand_label_key] = self.label return dct @staticmethod - def from_json(json_map: dict) -> 'Strand': # remove quotes when Py3.6 support dropped + def from_json(json_map: dict) -> Strand: substrand_jsons = mandatory_field(Strand, json_map, domains_key, legacy_keys=legacy_domains_keys) if len(substrand_jsons) == 0: raise IllegalDesignError(f'{domains_key} list cannot be empty') @@ -3350,7 +3442,7 @@ def from_json(json_map: dict) -> 'Strand': # remove quotes when Py3.6 support d label=label, ) - def __eq__(self, other: Any) -> bool: # remove quotes when Py3.6 support dropped + def __eq__(self, other: Any) -> bool: if not isinstance(other, Strand): return False return self.domains == other.domains @@ -3428,7 +3520,7 @@ def set_linear(self) -> None: """ self.set_circular(False) - def set_domains(self, domains: Iterable[Union[Domain[DomainLabel], Loopout[DomainLabel]]]) -> None: + def set_domains(self, domains: Iterable[Union[Domain, Loopout]]) -> None: """ Sets the :any:`Domain`'s/:any:`Loopout`'s of this :any:`Strand` to be `domains`, which can contain a mix of :any:`Domain`'s and :any:`Loopout`'s, @@ -3636,7 +3728,7 @@ def offset_3p(self) -> int: """3' offset of this entire :any:`Strand`, INCLUSIVE.""" return self.last_domain().offset_3p() - def overlaps(self, other: 'Strand') -> bool: # remove quotes when Py3.6 support dropped + def overlaps(self, other: Strand) -> bool: """Indicates whether `self` overlaps `other_strand`, meaning that the set of offsets occupied by `self` has nonempty intersection with those occupied by `other_strand`.""" for domain_self in self.bound_domains(): @@ -3645,7 +3737,7 @@ def overlaps(self, other: 'Strand') -> bool: # remove quotes when Py3.6 support return True return False - def assign_dna_complement_from(self, other: 'Strand') -> None: # remove quotes when Py3.6 support dropped + def assign_dna_complement_from(self, other: Strand) -> None: """Assuming a DNA sequence has been assigned to `other`, assign its Watson-Crick complement to the portions of this Strand that are bound to `other`. @@ -3903,7 +3995,7 @@ def idt_dna_sequence(self) -> str: return ''.join(ret_list) - def no_modifications_version(self) -> 'Strand': + def no_modifications_version(self) -> Strand: """ :return: version of this :any:`Strand` with no DNA modifications. """ @@ -4294,20 +4386,16 @@ def create_html_with_borders_tablefmt(): # type: ignore from functools import partial from tabulate import TableFormat, Line - html_with_borders_tablefmt = TableFormat( - lineabove=Line( - f"""\ + lineabove = Line(f"""\ \ - """, - "", - "", - "", - ), + """, "", "", "", ) + html_with_borders_tablefmt = TableFormat( # type: ignore + lineabove=lineabove, linebelowheader=None, linebetweenrows=None, linebelow=Line("
", "", "", ""), @@ -4746,8 +4834,8 @@ def find_overlapping_domains_on_helix(helix: Helix) -> List[Tuple[Domain, Domain else: reverse_domains.append(domain) - forward_domains.sort(key=lambda domain: domain.start) - reverse_domains.sort(key=lambda domain: domain.start) + forward_domains.sort(key=lambda dom: dom.start) + reverse_domains.sort(key=lambda dom: dom.start) if len(forward_domains) == 0 or len(reverse_domains) == 0: return [] @@ -4834,7 +4922,7 @@ def reverse_complementary(seq1: str, seq2: str, allow_wildcard: bool = False, :param seq1: first DNA sequence - :param seq1: + :param seq2: second DNA sequence :param allow_wildcard: if true a "wildcard" (the symbol '?') is considered to be complementary to anything @@ -4857,10 +4945,10 @@ def reverse_complementary(seq1: str, seq2: str, allow_wildcard: bool = False, @dataclass -class Design(_JSONSerializable, Generic[StrandLabel, DomainLabel]): +class Design(_JSONSerializable): """Object representing the entire design of the DNA structure.""" - strands: List[Strand[StrandLabel, DomainLabel]] + strands: List[Strand] """All of the :any:`Strand`'s in this :any:`Design`. Required field.""" @@ -5075,7 +5163,7 @@ def roll_of_helix(self, helix: Helix) -> float: return self.groups[helix.group].roll + helix.roll @staticmethod - def from_scadnano_file(filename: str) -> 'Design': # remove quotes when Py3.6 support dropped + def from_scadnano_file(filename: str) -> Design: """ Loads a :any:`Design` from the file with the given name. @@ -5087,7 +5175,7 @@ def from_scadnano_file(filename: str) -> 'Design': # remove quotes when Py3.6 s return Design.from_scadnano_json_str(json_str) @staticmethod - def from_scadnano_json_str(json_str: str) -> 'Design': # remove quotes when Py3.6 support dropped + def from_scadnano_json_str(json_str: str) -> Design: """ Loads a :any:`Design` from the given JSON string. @@ -5119,9 +5207,12 @@ def _num_helix_groups(json_map: Dict) -> int: return num_groups_used @staticmethod - def _helices_from_json(json_map: Dict) -> Tuple[List[Helix], - Dict[str, Tuple[float, float, int]], - Dict[Tuple[float, float], List[Helix]]]: + def _helices_from_json(json_map: Dict) \ + -> Tuple[ + List[Helix], + Dict[str, Tuple[float, float, int]], + Dict[Tuple[float, float], List[Helix]], + ]: """Returns list of helices as well as two maps, group_to_pitch_yaw, and pitch_yaw_to_helices group_to_pitch_yaw is filled if multiple helix groups are used @@ -5279,9 +5370,12 @@ def _groups_and_grid_from_json(json_map: dict, helices: List[Helix], return groups, grid @staticmethod - def _helices_and_groups_and_grid_from_json(json_map: Dict) -> Tuple[List[Helix], - Dict[str, HelixGroup], - Grid]: + def _helices_and_groups_and_grid_from_json(json_map: Dict) \ + -> Tuple[ + List[Helix], + Dict[str, HelixGroup], + Grid, + ]: helices, group_to_pitch_yaw, pitch_yaw_to_helices = Design._helices_from_json(json_map) groups, grid = Design._groups_and_grid_from_json(json_map, helices, group_to_pitch_yaw, pitch_yaw_to_helices) @@ -5289,7 +5383,7 @@ def _helices_and_groups_and_grid_from_json(json_map: Dict) -> Tuple[List[Helix], @staticmethod def from_scadnano_json_map( - json_map: dict) -> 'Design': # remove quotes when Py3.6 support dropped + json_map: dict) -> Design: """ Loads a :any:`Design` from the given JSON object (i.e., Python object obtained by calling json.loads(json_str) from a string representing contents of a JSON file. @@ -5449,12 +5543,24 @@ def base_pairs(self, allow_mismatches: bool = False) -> Dict[int, List[int]]: If a :any:`Helix` has no base pairs, then its :data:`Helix.idx` is not a key in the returned dict. + An offset with a deletion on either :any:`Domain` is not considered a base pair. + + Insertions are more complex. If `allow_mismatches` is False, then an offset with an insertion on + *both* :any:`Domain`'s is considered a *single* base pair so long as the DNA sequences on each + insertion are the same length and complementary. If `allow_mismatches` is True then an offset with + an insertion on *either* :any:`Domain`'s is considered a *single* base pair regardless of the + length or DNA sequences of either insertion. + + To calculate "true" base pairs in the presence of deletions and insertions, it is recommended + first to remove the deletions and insertions using the method + :meth:`Design.inline_deletions_insertions`. + :param allow_mismatches: if True, then all offsets on a :any:`Helix` where there is both a forward and reverse :any:`Domain` will be included. Otherwise, only offsets where the :any:`Domain`'s have complementary bases will be included. :return: - all base pairs (`helix_idx`, `offset`) in this :any:`Design` + dict mapping each helix_idx to a list of offsets on that helix where the base pairs are """ base_pairs = {} for idx, helix in self.helices.items(): @@ -5667,7 +5773,6 @@ def _cadnano_v2_import_explore_strand(vstrands: VStrands, return strand - # remove quotes when Py3.6 support dropped @staticmethod def from_cadnano_v2(directory: str = '', filename: Optional[str] = None, json_dict: Optional[dict] = None) -> 'Design': @@ -7182,7 +7287,7 @@ def to_oxview_format(self, warn_duplicate_strand_names: bool = True, :param warn_duplicate_strand_names: if True, prints a warning to the screen indicating when strands are found to have duplicate names. (default: True) - :param use_strand_color: + :param use_strand_colors: if True (default), sets the color of each nucleotide in a strand in oxView to the color of the strand. """ @@ -7194,7 +7299,7 @@ def to_oxview_format(self, warn_duplicate_strand_names: bool = True, nuc_count = 0 strand_count = 0 strand_nuc_start = [-1] - for strand1, oxdna_strand in zip(self.strands, system.strands): + for sc_strand, oxdna_strand in zip(self.strands, system.strands): strand_count += 1 oxvnucs: List[Dict[str, Any]] = [] strand_nuc_start.append(nuc_count) @@ -7203,8 +7308,8 @@ def to_oxview_format(self, warn_duplicate_strand_names: bool = True, 'end5': nuc_count, 'end3': nuc_count + len(oxdna_strand.nucleotides), 'monomers': oxvnucs} - if use_strand_colors and (strand1.color is not None): - scolor = strand1.color.to_cadnano_v2_int_hex() + if use_strand_colors and (sc_strand.color is not None): + scolor = sc_strand.color.to_cadnano_v2_int_hex() else: scolor = None @@ -7226,16 +7331,16 @@ def to_oxview_format(self, warn_duplicate_strand_names: bool = True, oxvnucs.append(oxvnuc) oxview_strands.append(oxvstrand) - for si1, (strand1, oxv_strand1) in enumerate(zip(self.strands, oxview_strands)): - for si2, strand2 in enumerate(self.strands): - if not strand1.overlaps(strand2): + for si1, (sc_strand1, oxv_strand1) in enumerate(zip(self.strands, oxview_strands)): + for si2, sc_strand2 in enumerate(self.strands): + if not sc_strand1.overlaps(sc_strand2): continue s1_nuc_idx = strand_nuc_start[si1 + 1] - for domain1 in strand1.domains: + for domain1 in sc_strand1.domains: if isinstance(domain1, (Loopout, Extension)): continue s2_nuc_idx = strand_nuc_start[si2 + 1] - for domain2 in strand2.domains: + for domain2 in sc_strand2.domains: if isinstance(domain2, (Loopout, Extension)): continue if not domain1.overlaps(domain2): @@ -7257,11 +7362,11 @@ def to_oxview_format(self, warn_duplicate_strand_names: bool = True, # to mismatch. (FIXME: this must be changed if scadnano later supports # degenerate base codes.) for d1, d2 in zip(d1range, d2range): - if ((strand1.dna_sequence is not None) and - (strand2.dna_sequence is not None) and - (strand1.dna_sequence[d1] != "?") and - (strand2.dna_sequence[d2] != "?") and - (wc(strand1.dna_sequence[d1]) != strand2.dna_sequence[d2])): + if ((sc_strand1.dna_sequence is not None) and + (sc_strand2.dna_sequence is not None) and + (sc_strand1.dna_sequence[d1] != "?") and + (sc_strand2.dna_sequence[d2] != "?") and + (wc(sc_strand1.dna_sequence[d1]) != sc_strand2.dna_sequence[d2])): continue oxv_strand1['monomers'][d1]['bp'] = s2_nuc_idx + d2 @@ -7289,7 +7394,7 @@ def write_oxview_file(self, directory: str = '.', filename: Optional[str] = None :param warn_duplicate_strand_names: if True, prints a warning to the screen indicating when strands are found to have duplicate names. (default: True) - :param use_strand_color: + :param use_strand_colors: if True (default), sets the color of each nucleotide in a strand in oxView to the color of the strand. """ @@ -7490,8 +7595,8 @@ def add_nick(self, helix: int, offset: int, forward: bool, new_color: bool = Tru order = domains.index(domain_to_remove) domains_before = domains[:order] domains_after = domains[order + 1:] - domain_left: Domain[DomainLabel] = Domain(helix, forward, domain_to_remove.start, offset) - domain_right: Domain[DomainLabel] = Domain(helix, forward, offset, domain_to_remove.end) + domain_left: Domain = Domain(helix, forward, domain_to_remove.start, offset) + domain_right: Domain = Domain(helix, forward, offset, domain_to_remove.end) # "before" and "after" mean in the 5' --> 3' direction, i.e., if a reverse domain: # <--------] @@ -7553,7 +7658,7 @@ def add_nick(self, helix: int, offset: int, forward: bool, new_color: bool = Tru self.strands.remove(strand) idt_present = strand.idt is not None - strand_before: Strand[StrandLabel, DomainLabel] = Strand( + strand_before: Strand = Strand( domains=domains_before, dna_sequence=seq_before_whole, color=strand.color, @@ -7561,7 +7666,7 @@ def add_nick(self, helix: int, offset: int, forward: bool, new_color: bool = Tru ) color_after = next(self.color_cycler) if new_color else strand.color - strand_after: Strand[StrandLabel, DomainLabel] = Strand( + strand_after: Strand = Strand( domains=domains_after, dna_sequence=seq_after_whole, color=color_after, @@ -7637,11 +7742,11 @@ def ligate(self, helix: int, offset: int, forward: bool) -> None: strand_left = dom_left.strand() strand_right = dom_right.strand() - dom_new: Domain[DomainLabel] = Domain(helix=helix, forward=forward, start=dom_left.start, - end=dom_right.end, - deletions=dom_left.deletions + dom_right.deletions, - insertions=dom_left.insertions + dom_right.insertions, - name=dom_left.name, label=dom_left.label) + dom_new: Domain = Domain(helix=helix, forward=forward, start=dom_left.start, + end=dom_right.end, + deletions=dom_left.deletions + dom_right.deletions, + insertions=dom_left.insertions + dom_right.insertions, + name=dom_left.name, label=dom_left.label) # normalize 5'/3' distinction; below refers to which Strand has the 5'/3' end that will be ligated # So strand_5p is the one whose 3' end will be the 3' end of the whole new Strand @@ -7807,9 +7912,9 @@ def add_half_crossover(self, helix: int, helix2: int, offset: int, forward: bool raise IllegalDesignError( 'cannot add crossover between two strands if one has a DNA sequence ' 'and the other does not') - new_strand: Strand[StrandLabel, DomainLabel] = Strand(domains=new_domains, color=strand_first.color, - dna_sequence=new_dna, idt=strand_first.idt, - is_scaffold=strand1.is_scaffold or strand2.is_scaffold) + new_strand: Strand = Strand(domains=new_domains, color=strand_first.color, + dna_sequence=new_dna, idt=strand_first.idt, + is_scaffold=strand1.is_scaffold or strand2.is_scaffold) # put new strand in place where strand_first was strand_first_idx = self.strands.index(strand_first) @@ -8028,7 +8133,12 @@ def _ensure_helix_groups_exist(self) -> None: f'{", ".join(self.groups.keys())}') def _has_default_groups(self) -> bool: - return len(self.groups) == 1 and default_group_name in self.groups + if not (len(self.groups) == 1 and default_group_name in self.groups): + return False + # even if there's only one group and it has the default name, + # need to check its fields for non-default values + group = self.groups[default_group_name] + return group.has_default_position_and_orientation() def _assign_default_helices_view_orders_to_groups(self) -> None: for name, group in self.groups.items(): @@ -8112,6 +8222,8 @@ def write_file_same_name_as_running_python_script(contents: str, extension: str, extension to use :param directory: directory in which to write file. If not specified, the current working directory is used. + :param add_extension: + whether to replace `.py` with `extension` :param filename: filename to use instead of the currently running script """ @@ -8514,10 +8626,10 @@ def _convert_design_to_oxdna_system(design: Design) -> _OxdnaSystem: # we have to check insertions first because they affect the index if offset in insertions: num = insertions[offset] - for i in range(num): + for j in range(num): cen = origin_ + forward * ( - offset + mod - num + i) * geometry.rise_per_base_pair * NM_TO_OX_UNITS - norm = normal.rotate(step_rot * (offset + mod - num + i), forward) + offset + mod - num + j) * geometry.rise_per_base_pair * NM_TO_OX_UNITS + norm = normal.rotate(step_rot * (offset + mod - num + j), forward) # note oxDNA n vector points 3' to 5' opposite of scadnano forward vector forw = -forward if domain.forward else forward nuc = _OxdnaNucleotide(cen, norm, forw, seq[index]) @@ -8543,7 +8655,7 @@ def _convert_design_to_oxdna_system(design: Design) -> _OxdnaSystem: elif isinstance(domain, Loopout): # we place the loopout nucleotides at temporary nonsense positions and orientations # these will be updated later, for now we just need the base - for i in range(domain.length): + for _ in range(domain.length): base = seq[i] center = _OxdnaVector() normal = _OxdnaVector(0, -1, 0) diff --git a/tests/scadnano_tests.py b/tests/scadnano_tests.py index 0f5ecf12..a8e3fd50 100644 --- a/tests/scadnano_tests.py +++ b/tests/scadnano_tests.py @@ -1,8107 +1,8137 @@ -import dataclasses -import os -import sys -import tempfile -import unittest -import re -import json -import math -from typing import Iterable, Union, Dict, Any - -import xlrd # type: ignore - -import scadnano as sc -import scadnano.origami_rectangle as rect -import scadnano.modifications as mod - -from scadnano.scadnano import _convert_design_to_oxdna_system - - -def strand_matching(strands: Iterable[sc.Strand], helix: int, forward: bool, start: int, - end: int) -> sc.Strand: - """ - Finds strand whose first bound domain matches the given parameters. - """ - return next(s for s in strands if - s.first_bound_domain().helix == helix and - s.first_bound_domain().forward == forward and - s.first_bound_domain().start == start and - s.first_bound_domain().end == end) - - -def remove_whitespace(sequence: str) -> str: - sequence = re.sub(r'\s*', '', sequence) - return sequence - - -class TestCreateStrandChainedMethods(unittest.TestCase): - # tests methods for creating strands using chained method notation as in this issue: - # https://github.com/UC-Davis-molecular-computing/scadnano-python-package/issues/85 - - def setUp(self) -> None: - helices = [sc.Helix(max_offset=100) for _ in range(6)] - self.design_6helix: sc.Design = sc.Design(helices=helices, strands=[], grid=sc.square) - - def test_strand__loopouts_with_labels(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.loopout(1, 8) - sb.with_domain_label('loop0') - sb.to(5) - sb.with_domain_label('dom1') - sb.cross(2) - sb.to(10) - sb.with_domain_label('dom2') - sb.loopout(3, 12) - sb.with_domain_label('loop1') - sb.to(5) - expected_strand = sc.Strand([ - sc.Domain(0, True, 0, 10), - sc.Loopout(8, label='loop0'), - sc.Domain(1, False, 5, 10, label='dom1'), - sc.Domain(2, True, 5, 10, label='dom2'), - sc.Loopout(12, label='loop1'), - sc.Domain(3, False, 5, 10), - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - - def test_strand__loopouts_with_labels_and_colors_to_json(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.loopout(1, 8) - sb.with_domain_color(sc.Color(10, 10, 10)) - sb.with_domain_label('loop0') - sb.to(5) - sb.with_domain_label('dom1') - sb.with_domain_color(sc.Color(20, 20, 20)) - sb.cross(2) - sb.to(10) - sb.with_domain_label('dom2') - sb.loopout(3, 12) - sb.with_domain_label('loop1') - sb.to(5) - sb.with_color(sc.Color(30, 30, 30)) - design_json_map = design.to_json_serializable(suppress_indent=False) - design_from_json = sc.Design.from_scadnano_json_map(design_json_map) - expected_strand = sc.Strand([ - sc.Domain(0, True, 0, 10), - sc.Loopout(8, label='loop0', color=sc.Color(10, 10, 10)), - sc.Domain(1, False, 5, 10, label='dom1', color=sc.Color(20, 20, 20)), - sc.Domain(2, True, 5, 10, label='dom2'), - sc.Loopout(12, label='loop1'), - sc.Domain(3, False, 5, 10), - ], color=sc.Color(30, 30, 30)) - self.assertEqual(1, len(design_from_json.strands)) - self.assertEqual(expected_strand, design_from_json.strands[0]) - self.assertEqual(expected_strand.color, sc.Color(30, 30, 30)) - - def test_strand__3p_extension(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - - sb.extension_3p(5) - sb.with_domain_color(sc.Color(10, 10, 10)) - - expected_strand: sc.Strand = sc.Strand([ - sc.Domain(0, True, 0, 10), - sc.Extension(num_bases=5, color=sc.Color(10, 10, 10)), - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - - def test_strand__5p_extension(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.extension_5p(5) - sb.to(10) - - expected_strand: sc.Strand = sc.Strand([ - sc.Extension(5), - sc.Domain(0, True, 0, 10), - ]) - - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - - def test_strand__update_to_after_5p_extension_ok(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.extension_5p(5) - - sb.to(10) - sb.update_to(15) - - expected_strand: sc.Strand = sc.Strand([ - sc.Extension(5), - sc.Domain(0, True, 0, 15), - ]) - - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - - def test_strand__move_after_5p_extension_ok(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.extension_5p(5) - - sb.move(15) - - expected_strand: sc.Strand = sc.Strand([ - sc.Extension(5), - sc.Domain(0, True, 0, 15), - ]) - - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - - def test_strand__to_after_3p_extension_should_raise_error(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.extension_3p(5) - - with self.assertRaises(sc.IllegalDesignError): - sb.to(15) - - def test_strand__move_after_3p_extension_should_raise_error(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.move(10) - sb.extension_3p(5) - - with self.assertRaises(sc.IllegalDesignError): - sb.move(5) - - def test_strand__cross_after_5p_extension_should_raise_error(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.extension_5p(5) - - with self.assertRaises(sc.IllegalDesignError): - sb.cross(1) - - def test_strand__cross_after_3p_extension_should_raise_error(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(5) - sb.extension_3p(5) - - with self.assertRaises(sc.IllegalDesignError): - sb.cross(1) - - def test_strand__extension_3p_after_loopout_should_raise_error(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.loopout(1, 3) - - with self.assertRaises(sc.IllegalDesignError): - sb.extension_3p(5) - - def test_strand__extension_3p_after_extension_should_raise_error(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.extension_3p(4) - - with self.assertRaises(sc.IllegalDesignError): - sb.extension_3p(5) - - def test_strand__update_to_after_3p_extension_should_raise_error(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.extension_3p(4) - - with self.assertRaises(sc.IllegalDesignError): - sb.update_to(15) - - def test_strand__as_circular_with_3p_extension_should_raise_error(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.extension_3p(4) - - with self.assertRaises(sc.IllegalDesignError): - sb.as_circular() - - def test_strand__as_circular_with_5p_extension_should_raise_error(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.extension_5p(4) - sb.to(10) - - with self.assertRaises(sc.IllegalDesignError): - sb.as_circular() - - def test_strand__extension_3p_on_circular_strand_should_raise_error(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.as_circular() - - with self.assertRaises(sc.IllegalDesignError): - sb.extension_3p(4) - - def test_strand__extension_3p_with_label(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.extension_3p(5) - sb.with_domain_label("ext1") - - expected_strand: sc.Strand = sc.Strand([ - sc.Domain(0, True, 0, 10), - sc.Extension(5, label="ext1"), - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - - def test_strand__extension_5p_with_label(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.extension_5p(5) - sb.with_domain_label("ext1") - sb.to(10) - - expected_strand: sc.Strand = sc.Strand([ - sc.Extension(5, label="ext1"), - sc.Domain(0, True, 0, 10) - ]) - - def test_strand__with_sequence_on_3p_extension(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.extension_3p(5) - sb.with_sequence("A" * 10 + "G" * 5) - - expected_strand: sc.Strand = sc.Strand([ - sc.Domain(0, True, 0, 10, dna_sequence="A" * 10), - sc.Extension(5, dna_sequence="G" * 5), - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - - def test_strand__with_sequence_on_5p_extension(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.extension_5p(5) - sb.to(10) - sb.with_sequence("C" * 5 + "T" * 10) - - expected_strand: sc.Strand = sc.Strand([ - sc.Extension(5, dna_sequence="C" * 5), - sc.Domain(0, True, 0, 10, dna_sequence="T" * 10), - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - - def test_strand__with_domain_sequence_on_extension(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.extension_3p(5) - sb.with_domain_sequence("G" * 5) - - expected_strand: sc.Strand = sc.Strand([ - sc.Domain(0, True, 0, 10, dna_sequence="?" * 10), - sc.Extension(5, dna_sequence="G" * 5), - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - - def test_strand__extension_with_name(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.extension_3p(5) - sb.with_domain_name("ext1") - - expected_strand: sc.Strand = sc.Strand([ - sc.Domain(0, True, 0, 10), - sc.Extension(5, name="ext1"), - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - - def test_strand__with_relative_offset(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0).to(10) - - sb.extension_3p(5, display_length=1.4, display_angle=30) - - expected_strand: sc.Strand = sc.Strand([ - sc.Domain(0, True, 0, 10), - sc.Extension(5, display_length=1.4, display_angle=30) - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - - def test_strand__0_0_to_10_cross_1_to_5(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.cross(1) - sb.to(5) - expected_strand = sc.Strand([ - sc.Domain(0, True, 0, 10), - sc.Domain(1, False, 5, 10), - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - self.assertEqual(1, len(design.helices[0].domains)) - self.assertEqual(1, len(design.helices[1].domains)) - self.assertEqual(0, len(design.helices[2].domains)) - self.assertEqual(0, len(design.helices[3].domains)) - self.assertEqual(0, len(design.helices[4].domains)) - self.assertEqual(0, len(design.helices[5].domains)) - - def test_strand__0_0_to_10_cross_1_to_5__reverse(self) -> None: - design = self.design_6helix - design.draw_strand(1, 5).to(10).cross(0).to(0) - expected_strand = sc.Strand([ - sc.Domain(1, True, 5, 10), - sc.Domain(0, False, 0, 10), - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - self.assertEqual(1, len(design.helices[0].domains)) - self.assertEqual(1, len(design.helices[1].domains)) - self.assertEqual(0, len(design.helices[2].domains)) - self.assertEqual(0, len(design.helices[3].domains)) - self.assertEqual(0, len(design.helices[4].domains)) - self.assertEqual(0, len(design.helices[5].domains)) - - def test_strand__h0_off0_to_off10_cross_h1_to_off5_loopout_length3_h2_to_off15(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.cross(1) - sb.to(5) - sb.loopout(2, 3) - sb.to(15) - expected_strand = sc.Strand([ - sc.Domain(0, True, 0, 10), - sc.Domain(1, False, 5, 10), - sc.Loopout(3), - sc.Domain(2, True, 5, 15), - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - self.assertEqual(1, len(design.helices[0].domains)) - self.assertEqual(1, len(design.helices[1].domains)) - self.assertEqual(1, len(design.helices[2].domains)) - self.assertEqual(0, len(design.helices[3].domains)) - self.assertEqual(0, len(design.helices[4].domains)) - self.assertEqual(0, len(design.helices[5].domains)) - - def test_strand__two_forward_paranemic_crossovers(self) -> None: - design = self.design_6helix - design.draw_strand(0, 0).to(10).cross(1).to(15).cross(2).to(20) - expected_strand = sc.Strand([ - sc.Domain(0, True, 0, 10), - sc.Domain(1, True, 10, 15), - sc.Domain(2, True, 15, 20), - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - self.assertEqual(1, len(design.helices[0].domains)) - self.assertEqual(1, len(design.helices[1].domains)) - self.assertEqual(1, len(design.helices[2].domains)) - self.assertEqual(0, len(design.helices[3].domains)) - self.assertEqual(0, len(design.helices[4].domains)) - self.assertEqual(0, len(design.helices[5].domains)) - - def test_strand__two_reverse_paranemic_crossovers(self) -> None: - design = self.design_6helix - design.draw_strand(0, 20).to(10).cross(1).to(5).cross(2).to(0) - expected_strand = sc.Strand([ - sc.Domain(0, False, 10, 20), - sc.Domain(1, False, 5, 10), - sc.Domain(2, False, 0, 5), - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - self.assertEqual(1, len(design.helices[0].domains)) - self.assertEqual(1, len(design.helices[1].domains)) - self.assertEqual(1, len(design.helices[2].domains)) - self.assertEqual(0, len(design.helices[3].domains)) - self.assertEqual(0, len(design.helices[4].domains)) - self.assertEqual(0, len(design.helices[5].domains)) - - def test_strand__multiple_strands(self) -> None: - design = self.design_6helix - design.draw_strand(0, 0).to(10).cross(1).to(0) - design.draw_strand(0, 20).to(10).cross(1).to(20) - expected_strand0 = sc.Strand([ - sc.Domain(0, True, 0, 10), - sc.Domain(1, False, 0, 10), - ]) - expected_strand1 = sc.Strand([ - sc.Domain(0, False, 10, 20), - sc.Domain(1, True, 10, 20), - ]) - self.assertEqual(2, len(design.strands)) - self.assertEqual(expected_strand0, design.strands[0]) - self.assertEqual(expected_strand1, design.strands[1]) - self.assertEqual(2, len(design.helices[0].domains)) - self.assertEqual(2, len(design.helices[1].domains)) - self.assertEqual(0, len(design.helices[2].domains)) - self.assertEqual(0, len(design.helices[3].domains)) - self.assertEqual(0, len(design.helices[4].domains)) - self.assertEqual(0, len(design.helices[5].domains)) - - def test_strand__multiple_strands_other_order(self) -> None: - design = self.design_6helix - design.draw_strand(0, 20).to(10).cross(1).to(20) - design.draw_strand(0, 0).to(10).cross(1).to(0) - expected_strand0 = sc.Strand([ - sc.Domain(0, False, 10, 20), - sc.Domain(1, True, 10, 20), - ]) - expected_strand1 = sc.Strand([ - sc.Domain(0, True, 0, 10), - sc.Domain(1, False, 0, 10), - ]) - self.assertEqual(2, len(design.strands)) - self.assertEqual(expected_strand0, design.strands[0]) - self.assertEqual(expected_strand1, design.strands[1]) - self.assertEqual(2, len(design.helices[0].domains)) - self.assertEqual(2, len(design.helices[1].domains)) - self.assertEqual(0, len(design.helices[2].domains)) - self.assertEqual(0, len(design.helices[3].domains)) - self.assertEqual(0, len(design.helices[4].domains)) - self.assertEqual(0, len(design.helices[5].domains)) - - def test_strand__multiple_strands_overlap_no_error(self) -> None: - design = self.design_6helix - design.draw_strand(0, 0).to(10).cross(1).to(0) \ - .as_scaffold() \ - .with_modification_internal(5, mod.cy3_int, warn_on_no_dna=False) - design.draw_strand(0, 10).to(0).cross(1).to(10).with_modification_5p(mod.biotin_5p) - expected_strand0 = sc.Strand([ - sc.Domain(0, True, 0, 10), - sc.Domain(1, False, 0, 10), - ], is_scaffold=True) - expected_strand1 = sc.Strand([ - sc.Domain(0, False, 0, 10), - sc.Domain(1, True, 0, 10), - ]) - - expected_strand0.set_modification_internal(5, mod.cy3_int, warn_on_no_dna=False) - expected_strand1.set_modification_5p(mod.biotin_5p) - - self.assertEqual(2, len(design.strands)) - - self.assertEqual(expected_strand0, design.strands[0]) - self.assertEqual(None, design.strands[0].modification_5p) - self.assertEqual(None, design.strands[0].modification_3p) - self.assertDictEqual({5: mod.cy3_int}, design.strands[0].modifications_int) - - self.assertEqual(expected_strand1, design.strands[1]) - self.assertEqual(mod.biotin_5p, design.strands[1].modification_5p) - self.assertEqual(None, design.strands[1].modification_3p) - self.assertDictEqual({}, design.strands[1].modifications_int) - - self.assertEqual(2, len(design.helices[0].domains)) - self.assertEqual(2, len(design.helices[1].domains)) - self.assertEqual(0, len(design.helices[2].domains)) - self.assertEqual(0, len(design.helices[3].domains)) - self.assertEqual(0, len(design.helices[4].domains)) - self.assertEqual(0, len(design.helices[5].domains)) - - def test_strand__multiple_strands_overlap_error(self) -> None: - design = self.design_6helix - design.draw_strand(0, 0).to(10).cross(1).to(0) - with self.assertRaises(sc.IllegalDesignError): - design.draw_strand(0, 2).to(8) - - def test_strand__call_to_twice_legally(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.cross(1) - sb.to(5) - sb.to(0) - expected_strand = sc.Strand([ - sc.Domain(0, True, 0, 10), - sc.Domain(1, False, 5, 10), - sc.Domain(1, False, 0, 5), - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - self.assertEqual(1, len(design.helices[0].domains)) - self.assertEqual(2, len(design.helices[1].domains)) - self.assertEqual(0, len(design.helices[2].domains)) - self.assertEqual(0, len(design.helices[3].domains)) - self.assertEqual(0, len(design.helices[4].domains)) - self.assertEqual(0, len(design.helices[5].domains)) - - def test_strand__call_update_to_twice_legally(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.cross(1) - sb.update_to(5) - sb.update_to(0) - expected_strand = sc.Strand([ - sc.Domain(0, True, 0, 10), - sc.Domain(1, False, 0, 10), - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - self.assertEqual(1, len(design.helices[0].domains)) - self.assertEqual(1, len(design.helices[1].domains)) - self.assertEqual(0, len(design.helices[2].domains)) - self.assertEqual(0, len(design.helices[3].domains)) - self.assertEqual(0, len(design.helices[4].domains)) - self.assertEqual(0, len(design.helices[5].domains)) - - def test_strand__call_to_then_update_to_legally(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - sb.cross(1) - sb.to(5) - sb.update_to(0) - expected_strand = sc.Strand([ - sc.Domain(0, True, 0, 10), - sc.Domain(1, False, 0, 10), - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - self.assertEqual(1, len(design.helices[0].domains)) - self.assertEqual(1, len(design.helices[1].domains)) - self.assertEqual(0, len(design.helices[2].domains)) - self.assertEqual(0, len(design.helices[3].domains)) - self.assertEqual(0, len(design.helices[4].domains)) - self.assertEqual(0, len(design.helices[5].domains)) - - def test_strand__call_to_twice_increase_decrease_forward(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 0) - sb.to(10) - with self.assertRaises(sc.IllegalDesignError): - sb.to(5) - - def test_strand__call_to_twice_decrease_increase_reverse(self) -> None: - design = self.design_6helix - sb = design.draw_strand(0, 10) - sb.to(0) - with self.assertRaises(sc.IllegalDesignError): - sb.to(5) - - -class TestCreateHelix(unittest.TestCase): - - def test_helix_constructor_no_max_offset_with_major_ticks(self) -> None: - # tests bug where an exception is raised if major ticks is defined but not max_offset - sc.Helix(major_ticks=[0, 5, 10]) - - -class TestM13(unittest.TestCase): - - def test_p7249(self) -> None: - p7249 = sc.m13() - self.assertEqual('TTCCCTTCCTTTCTCG', p7249[:16]) - self.assertEqual(7249, len(p7249)) - p7249 = sc.m13(rotation=0) - self.assertEqual('AATGCTACTACTATTA', p7249[:16]) - self.assertEqual(7249, len(p7249)) - - def test_p7560(self) -> None: - p7560 = sc.m13(rotation=0, variant=sc.M13Variant.p7560) - self.assertEqual('AGCTTGGCACTGGCCG', p7560[:16]) - self.assertEqual(7560, len(p7560)) - - def test_p8064(self) -> None: - p8064 = sc.m13(rotation=0, variant=sc.M13Variant.p8064) - self.assertEqual('GGCAATGACCTGATAG', p8064[:16]) - self.assertEqual(8064, len(p8064)) - - -class TestModifications(unittest.TestCase): - - def test_to_json__names_unique_for_modifications_raises_no_error(self) -> None: - helices = [sc.Helix(max_offset=100)] - design: sc.Design = sc.Design(helices=helices, strands=[], grid=sc.square) - name = 'mod_name' - design.draw_strand(0, 0).move(5).with_modification_5p( - sc.Modification5Prime(display_text=name, id=name)) - design.draw_strand(0, 5).move(5).with_modification_3p( - sc.Modification3Prime(display_text=name, id=name + '3')) - design.to_json(True) - - def test_to_json__names_not_unique_for_modifications_raises_error(self) -> None: - helices = [sc.Helix(max_offset=100)] - design: sc.Design = sc.Design(helices=helices, strands=[], grid=sc.square) - name = 'mod_name' - design.draw_strand(0, 0).move(5).with_modification_5p( - sc.Modification5Prime(display_text=name, id=name)) - design.draw_strand(0, 5).move(5).with_modification_3p( - sc.Modification3Prime(display_text=name, id=name)) - with self.assertRaises(sc.IllegalDesignError): - design.to_json(True) - - def test_mod_illegal_exceptions_raised(self) -> None: - strand = sc.Strand(domains=[sc.Domain(0, True, 0, 5)], dna_sequence='AATGC') - strand.set_modification_internal(2, mod.biotin_int) - with self.assertRaises(sc.IllegalDesignError): - strand.set_modification_internal(1, mod.biotin_int) - - # biotin3_1 = mod.Biotin(location=sc.ModLocation.prime3) - # biotin3_2 = mod.Biotin(location=sc.ModLocation.prime3) - # with self.assertRaises(sc.IllegalDesignError): - # strand = sc.Strand(domains=[sc.Substrand(0, True, 0, 5)], dna_sequence='AATGC', - # modifications=[biotin3_1, biotin3_2]) - # - # biotin_i_1 = mod.Biotin(location=sc.ModLocation.internal, offset=2) - # biotin_i_2 = mod.Biotin(location=sc.ModLocation.internal, offset=2) - # with self.assertRaises(sc.IllegalDesignError): - # strand = sc.Strand(domains=[sc.Substrand(0, True, 0, 5)], dna_sequence='AATGC', - # modifications=[biotin_i_1, biotin_i_2]) - # - # biotin_i_small = mod.Biotin(location=sc.ModLocation.internal, offset=-1) - # with self.assertRaises(sc.IllegalDesignError): - # strand = sc.Strand(domains=[sc.Substrand(0, True, 0, 5)], dna_sequence='AATGC', - # modifications=[biotin_i_small]) - # seq = strand.idt_dna_sequence() - # - # biotin_i_large = mod.Biotin(location=sc.ModLocation.internal, offset=10) - # with self.assertRaises(sc.IllegalDesignError): - # strand = sc.Strand(domains=[sc.Substrand(0, True, 0, 5)], dna_sequence='AATGC', - # modifications=[biotin_i_small]) - # seq = strand.idt_dna_sequence() - # - # biotin_i_offset_not_T = mod.Biotin(location=sc.ModLocation.internal, offset=0) - # with self.assertRaises(sc.IllegalDesignError): - # strand = sc.Strand(domains=[sc.Substrand(0, True, 0, 5)], dna_sequence='AATGC', - # modifications=[biotin_i_offset_not_T]) - # seq = strand.idt_dna_sequence() - # - # cy3I_offset_off_end = mod.Cy3(location=sc.ModLocation.internal, offset=4) - # with self.assertRaises(sc.IllegalDesignError): - # strand = sc.Strand(domains=[sc.Substrand(0, True, 0, 5)], dna_sequence='AATGC', - # modifications=[cy3I_offset_off_end]) - # seq = strand.idt_dna_sequence() - - def test_Cy3(self) -> None: - cy3_5 = mod.cy3_5p - self.assertEqual(r'/5Cy3/', cy3_5.idt_text) - self.assertEqual(r'/5Cy3/', cy3_5.id) - self.assertEqual('Cy3', cy3_5.display_text) - cy3_3 = mod.cy3_3p - self.assertEqual(r'/3Cy3Sp/', cy3_3.idt_text) - self.assertEqual(r'/3Cy3Sp/', cy3_3.id) - self.assertEqual('Cy3', cy3_3.display_text) - # cy3_i1 = mod.Cy3(location=sc.ModLocation.internal, offset=1) - cy3_i1 = mod.cy3_int - self.assertEqual(r'/iCy3/', cy3_i1.idt_text) - self.assertEqual(r'/iCy3/', cy3_i1.id) - self.assertEqual('Cy3', cy3_i1.display_text) - # cy3_i2 = mod.Cy3(location=sc.ModLocation.internal, offset=3) - cy3_i2 = mod.cy3_int - self.assertEqual(r'/iCy3/', cy3_i2.idt_text) - self.assertEqual(r'/iCy3/', cy3_i2.id) - self.assertEqual('Cy3', cy3_i2.display_text) - - strand5 = sc.Strand(domains=[sc.Domain(0, True, 0, 5)], dna_sequence='ATTGC', - modification_5p=cy3_5) - strand3 = sc.Strand(domains=[sc.Domain(1, True, 0, 5)], dna_sequence='ATTGC', - modification_3p=cy3_3) - strand_i = sc.Strand(domains=[sc.Domain(2, True, 0, 5)], dna_sequence='ATTGC', - modifications_int={1: cy3_i1, 3: cy3_i2}) - strand53 = sc.Strand(domains=[sc.Domain(3, True, 0, 5)], dna_sequence='ATTGC', - modification_5p=cy3_5, modification_3p=cy3_3) - strand53_i = sc.Strand(domains=[sc.Domain(4, True, 0, 5)], dna_sequence='ATTGC', - modification_5p=cy3_5, modification_3p=cy3_3, - modifications_int={1: cy3_i1, 3: cy3_i2}) - - self.assertEqual(r'/5Cy3/ATTGC', strand5.idt_dna_sequence()) - self.assertEqual(r'ATTGC/3Cy3Sp/', strand3.idt_dna_sequence()) - self.assertEqual(r'/5Cy3/ATTGC/3Cy3Sp/', strand53.idt_dna_sequence()) - self.assertEqual(r'AT/iCy3/TG/iCy3/C', strand_i.idt_dna_sequence()) - self.assertEqual(r'/5Cy3/AT/iCy3/TG/iCy3/C/3Cy3Sp/', strand53_i.idt_dna_sequence()) - - def test_biotin(self) -> None: - biotin5 = mod.biotin_5p - self.assertEqual(r'/5Biosg/', biotin5.idt_text) - self.assertEqual(r'/5Biosg/', biotin5.id) - self.assertEqual('B', biotin5.display_text) - biotin3 = mod.biotin_3p - self.assertEqual(r'/3Bio/', biotin3.idt_text) - self.assertEqual(r'/3Bio/', biotin3.id) - self.assertEqual('B', biotin3.display_text) - # biotin_i_1 = mod.Biotin(location=sc.ModLocation.internal, offset=1) - biotin_i_1 = mod.biotin_int - self.assertEqual(r'/iBiodT/', biotin_i_1.idt_text) - self.assertEqual(r'/iBiodT/', biotin_i_1.id) - self.assertEqual('B', biotin_i_1.display_text) - # biotin_i_2 = mod.Biotin(location=sc.ModLocation.internal, offset=2) - biotin_i_2 = mod.biotin_int - self.assertEqual(r'/iBiodT/', biotin_i_2.idt_text) - self.assertEqual(r'/iBiodT/', biotin_i_2.id) - self.assertEqual('B', biotin_i_2.display_text) - - strand5 = sc.Strand(domains=[sc.Domain(0, True, 0, 5)], dna_sequence='ATTGC', - modification_5p=biotin5) - strand3 = sc.Strand(domains=[sc.Domain(1, True, 0, 5)], dna_sequence='ATTGC', - modification_3p=biotin3) - strand_i = sc.Strand(domains=[sc.Domain(2, True, 0, 5)], dna_sequence='ATTGC', - modifications_int={1: biotin_i_1, 2: biotin_i_2}) - strand53 = sc.Strand(domains=[sc.Domain(3, True, 0, 5)], dna_sequence='ATTGC', - modification_5p=biotin5, modification_3p=biotin3) - strand53i = sc.Strand(domains=[sc.Domain(4, True, 0, 5)], dna_sequence='ATTGC', - modification_5p=biotin5, modification_3p=biotin3, - modifications_int={1: biotin_i_1, 2: biotin_i_2}) - self.assertEqual(r'/5Biosg/ATTGC', strand5.idt_dna_sequence()) - self.assertEqual(r'ATTGC/3Bio/', strand3.idt_dna_sequence()) - self.assertEqual(r'A/iBiodT//iBiodT/GC', strand_i.idt_dna_sequence()) - self.assertEqual(r'/5Biosg/ATTGC/3Bio/', strand53.idt_dna_sequence()) - self.assertEqual(r'/5Biosg/A/iBiodT//iBiodT/GC/3Bio/', strand53i.idt_dna_sequence()) - - def test_to_json_serializable(self) -> None: - biotin5 = mod.biotin_5p - biotin5 = dataclasses.replace(biotin5, connector_length=6) - self.assertEqual(r'/5Biosg/', biotin5.idt_text) - self.assertEqual(r'/5Biosg/', biotin5.id) - self.assertEqual('B', biotin5.display_text) - self.assertEqual(6, biotin5.connector_length) - biotin3 = mod.biotin_3p - self.assertEqual(r'/3Bio/', biotin3.idt_text) - self.assertEqual(r'/3Bio/', biotin3.id) - self.assertEqual('B', biotin3.display_text) - # biotin_i_1 = mod.Biotin(location=sc.ModLocation.internal, offset=1) - biotin_i_1 = mod.biotin_int - self.assertEqual(r'/iBiodT/', biotin_i_1.idt_text) - self.assertEqual(r'/iBiodT/', biotin_i_1.id) - self.assertEqual('B', biotin_i_1.display_text) - # biotin_i_2 = mod.Biotin(location=sc.ModLocation.internal, offset=2) - biotin_i_2 = mod.biotin_int - self.assertEqual(r'/iBiodT/', biotin_i_2.idt_text) - self.assertEqual(r'/iBiodT/', biotin_i_2.id) - self.assertEqual('B', biotin_i_2.display_text) - - strand5 = sc.Strand(domains=[sc.Domain(0, True, 0, 5)], dna_sequence='ATTGC', - modification_5p=biotin5) - strand3 = sc.Strand(domains=[sc.Domain(1, True, 0, 5)], dna_sequence='ATTGC', - modification_3p=biotin3) - strand_i = sc.Strand(domains=[sc.Domain(2, True, 0, 5)], dna_sequence='ATTGC', - modifications_int={1: biotin_i_1, 2: biotin_i_2}) - strand53 = sc.Strand(domains=[sc.Domain(3, True, 0, 5)], dna_sequence='ATTGC', - modification_5p=biotin5, modification_3p=biotin3) - strand53_i = sc.Strand(domains=[sc.Domain(4, True, 0, 5)], dna_sequence='ATTGC', - modification_5p=biotin5, modification_3p=biotin3, - modifications_int={1: biotin_i_1, 2: biotin_i_2}) - - strands = [strand5, strand3, strand_i, strand53, strand53_i] - design = sc.Design(strands=strands, grid=sc.square) - - # print(design.to_json()) - - json_dict = design.to_json_serializable(suppress_indent=False) - self.assertTrue(sc.design_modifications_key in json_dict) - mods_dict = json_dict[sc.design_modifications_key] - self.assertTrue(r'/5Biosg/' in mods_dict) - self.assertTrue(r'/3Bio/' in mods_dict) - self.assertTrue(r'/iBiodT/' in mods_dict) - - biotin5_json = mods_dict[r'/5Biosg/'] - self.assertEqual('/5Biosg/', biotin5_json[sc.mod_idt_text_key]) - self.assertEqual('B', biotin5_json[sc.mod_display_text_key]) - self.assertEqual(6, biotin5_json[sc.mod_connector_length_key]) - - biotin3_json = mods_dict[r'/3Bio/'] - self.assertEqual('/3Bio/', biotin3_json[sc.mod_idt_text_key]) - self.assertEqual('B', biotin3_json[sc.mod_display_text_key]) - self.assertNotIn(sc.mod_connector_length_key, biotin3_json) - - strand5_mod5_json = json_dict[sc.strands_key][0][sc.modification_5p_key] - strand3_mod3_json = json_dict[sc.strands_key][1][sc.modification_3p_key] - self.assertEqual("/5Biosg/", strand5_mod5_json) - self.assertEqual("/3Bio/", strand3_mod3_json) - - strand_i_mods_int_json = json_dict[sc.strands_key][2][sc.modifications_int_key] - self.assertDictEqual({"1": "/iBiodT/", "2": "/iBiodT/"}, strand_i_mods_int_json) - - strand53_mod5_json = json_dict[sc.strands_key][3][sc.modification_5p_key] - strand53_mod3_json = json_dict[sc.strands_key][3][sc.modification_3p_key] - self.assertEqual("/5Biosg/", strand53_mod5_json) - self.assertEqual("/3Bio/", strand53_mod3_json) - - strand53_i_mod5_json = json_dict[sc.strands_key][4][sc.modification_5p_key] - strand53_i_mod3_json = json_dict[sc.strands_key][4][sc.modification_3p_key] - strand53_i_mods_int_json = json_dict[sc.strands_key][4][sc.modifications_int_key] - self.assertEqual("/5Biosg/", strand53_i_mod5_json) - self.assertEqual("/3Bio/", strand53_i_mod3_json) - self.assertDictEqual({"1": "/iBiodT/", "2": "/iBiodT/"}, strand53_i_mods_int_json) - - -class TestImportCadnanoV2(unittest.TestCase): - """ - Tests the import feature to cadnano v2 (see misc/cadnano-format-specs/v2.txt). - """ - folder = "cadnano_v2_import" - input_path = os.path.join('tests_inputs', folder) - # This variable is used for writing out files, which can be useful for debugging - output_path = os.path.join('tests_outputs', folder) - - def test_32_helix_rectangle(self) -> None: - design = sc.Design.from_cadnano_v2(directory=self.input_path, - filename='test_32_helix_rectangle.json') - self.assertEqual(35, len(design.helices)) - # To help with debugging, uncomment these lines to write out the - # scadnano file - # - design.write_scadnano_file(directory=self.output_path, - filename=f'test_32_helix_rectangle.{sc.default_scadnano_file_extension}') - - def test_helices_order(self) -> None: - design = sc.Design.from_cadnano_v2(directory=self.input_path, - filename='test_helices_order.json') - self.assertEqual(11, len(design.helices)) - # To help with debugging, uncomment these lines to write out the - # scadnano file - # - design.write_scadnano_file(directory=self.output_path, - filename=f'test_helices_order.{sc.default_scadnano_file_extension}') - - def test_helices_order2(self) -> None: - design = sc.Design.from_cadnano_v2(directory=self.input_path, - filename='test_helices_order2.json') - self.assertEqual(35, len(design.helices)) - # To help with debugging, uncomment these lines to write out the - # scadnano file - # - design.write_scadnano_file(directory=self.output_path, - filename=f'test_helices_order2.{sc.default_scadnano_file_extension}') - - def test_huge_hex(self) -> None: - design = sc.Design.from_cadnano_v2(directory=self.input_path, - filename='test_huge_hex.json') - self.assertEqual(26, len(design.helices)) - # To help with debugging, uncomment these lines to write out the - # scadnano file - # - design.write_scadnano_file(directory=self.output_path, - filename=f'test_huge_hex.{sc.default_scadnano_file_extension}') - - def test_Science09_prot120_98_v3(self) -> None: - file_name = "test_Science09_prot120_98_v3" - design = sc.Design.from_cadnano_v2(directory=self.input_path, - filename=file_name + ".json") - self.assertEqual(19, len(design.helices)) - # To help with debugging, uncomment these lines to write out the - # scadnano file - # - # design.write_scadnano_file(directory=self.output_path, - # filename=f'{file_name}.{sc.default_scadnano_file_extension}') - - def test_Nature09_monolith(self) -> None: - file_name = "test_Nature09_monolith" - design = sc.Design.from_cadnano_v2(directory=self.input_path, - filename=file_name + ".json") - self.assertEqual(60, len(design.helices)) - # Verify snippets of the design - self.assertEqual(4, len(design.strands_starting_on_helix(53))) - self.assertEqual(5, len(design.strands_starting_on_helix(42))) - # To help with debugging, uncomment these lines to write out the - # scadnano file - # - design.write_scadnano_file(directory=self.output_path, - filename=f'{file_name}.{sc.default_scadnano_file_extension}') - - def test_circular_auto_staple(self) -> None: - file_name = "test_circular_auto_staple" - design = sc.Design.from_cadnano_v2(directory=self.input_path, - filename=file_name + ".json") - self.assertEqual(8, len(design.helices)) - # To help with debugging, uncomment these lines to write out the - # scadnano file - # - design.write_scadnano_file(directory=self.output_path, - filename=f'{file_name}.{sc.default_scadnano_file_extension}') - - def test_circular_auto_staple_hex(self) -> None: - file_name = "test_circular_auto_staple_hex" - design = sc.Design.from_cadnano_v2(directory=self.input_path, - filename=file_name + ".json") - self.assertEqual(6, len(design.helices)) - # To help with debugging, uncomment these lines to write out the - # scadnano file - # - design.write_scadnano_file(directory=self.output_path, - filename=f'{file_name}.{sc.default_scadnano_file_extension}') - - def test_paranemic_crossover(self) -> None: - file_name = "test_crossover_to_same_helix" - design = sc.Design.from_cadnano_v2(directory=self.input_path, - filename=file_name + ".json") - self.assertEqual(1, len(design.helices)) - # To help with debugging, uncomment these lines to write out the - # scadnano file - # - design.write_scadnano_file(directory=self.output_path, - filename=f'{file_name}.{sc.default_scadnano_file_extension}') - - def test_same_helix_crossover(self) -> None: - file_name = "test_paranemic_crossover" - design = sc.Design.from_cadnano_v2(directory=self.input_path, - filename=file_name + ".json") - self.assertEqual(4, len(design.helices)) - - def test_2_stape_2_helix_origami_deletions_insertions(self) -> None: - file_name = "test_2_stape_2_helix_origami_deletions_insertions" - design = sc.Design.from_cadnano_v2(directory=self.input_path, - filename=file_name + ".json") - self.assertEqual(2, len(design.helices)) - self.assertEqual(design.grid, sc.Grid.square) - self.assertEqual(2, len(design.helices)) - output_helix_0 = design.helices[0] - output_helix_1 = design.helices[1] - self.assertEqual(output_helix_0.grid_position, (0, 0)) - self.assertEqual(output_helix_1.grid_position, (0, 1)) - self.assertEqual(3, len(design.strands)) - - # left staple - stap_left_ss1 = sc.Domain(helix=1, forward=True, start=0, end=16, deletions=[12], insertions=[(6, 3)]) - stap_left_ss0 = sc.Domain(helix=0, forward=False, start=0, end=16, deletions=[11, 12], - insertions=[(6, 1)]) - stap_left = sc.Strand(domains=[stap_left_ss1, stap_left_ss0]) - self.assertIn(stap_left, design.strands) - - # right staple - stap_right_ss0 = sc.Domain(helix=0, forward=False, start=16, end=32, deletions=[24], - insertions=[(18, 2)]) - stap_right_ss1 = sc.Domain(helix=1, forward=True, start=16, end=32, deletions=[24], - insertions=[(18, 4)]) - stap_right = sc.Strand(domains=[stap_right_ss0, stap_right_ss1]) - self.assertIn(stap_right, design.strands) - - # scaffold - scaf_ss1_left = sc.Domain(helix=1, forward=False, start=0, end=16, deletions=[12], - insertions=[(6, 3)]) - scaf_ss0 = sc.Domain(helix=0, forward=True, start=0, end=32, deletions=[11, 12, 24], - insertions=[(6, 1), (18, 2)]) - # loopout = sc.Loopout(length=3) No loopout in cadnano - scaf_ss1_right = sc.Domain(helix=1, forward=False, start=16, end=32, deletions=[24], - insertions=[(18, 4)]) - scaf = sc.Strand(domains=[scaf_ss1_left, scaf_ss0, scaf_ss1_right], is_scaffold=True) - self.assertIn(scaf, design.strands) - - # To help with debugging, uncomment these lines to write out the - # scadnano file - # - design.write_scadnano_file(directory=self.output_path, - filename=f'{file_name}.{sc.default_scadnano_file_extension}') - - -class TestExportDNASequences(unittest.TestCase): - - def setUp(self) -> None: - r""" Removing scaffold from this design: - 0 8 16 24 32 40 48 56 64 72 80 88 96 -0 +------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------+ - /<------- -------+ +------- -------- -------] <------- -------- -------- -------] <------+ +------- -------]\ - ( | | | | ) -1 \[------- -------+ +------> [------+ +------- -------- -------> [------+ +------- -------+ +------- ------->/ - +------- -------- -------- -------- -------- -------+ +------- -------- -------- -------- -------- -------+ - | | | | | | -2 +------- -------- -------- -------- -------- -------+ +------- -------- -------- -------- -------- -------+ - /<------- -------+ +------- -------+ +------] <------- -------- -------+ +------] <------+ +------- -------]\ - ( | | | | ) -3 \[------- -------+ +------> [------+ +------- -------- -------> [------+ +------- -------+ +------- ------->/ - +------- -------- -------- -------- -------- -------+ +------- -------- -------- -------- -------- -------+ - | | | | | | -4 +------- -------- -------- -------- -------- -------+ +------- -------- -------- -------- -------- -------+ - /<------- -------+ +------- -------+ +------] <------- -------- -------+ +------] <------+ +------- -------]\ - ( | | | | ) -5 \[------- -------+ +------> [------- -------- -------- -------> [------- -------- -------+ +------- ------->/ - +------- -------- -------- -------- -------- -------] <------- -------- -------- -------- -------- -------+ - -gives this (removed scaffold to make it easier for me to visually track the strands) - - 0 8 16 24 32 40 48 56 64 72 80 88 96 -0 <-------A-------+ +-------D--------D-------] <-------G--------G--------G-------] <------+ +-------P-------] - | | | | -1 [-------A-------+ +------> [------+ +-------H--------H-------> [------+ +-------M-------+ +-------P-------> - | | | | -2 <-------B-------+ +-------E-------+ +------] <-------I--------I-------+ +------] <------+ +-------Q-------] - | | | | -3 [-------B-------+ +------> [------+ +-------J--------J-------> [------+ +-------N-------+ +-------Q-------> - | | | | -4 <-------C-------+ +-------F-------+ +------] <-------K--------K-------+ +------] <------+ +-------R-------] - | | | | -5 [-------C-------+ +------> [-------L--------L--------L-------> [-------O--------O-------+ +-------R-------> - -We give the strands simple single-letter names to help test they appear in the correct order in the output. - -row major 5': DGPAEIHMQBFKJNRCLO -col major 5': ABCEFLDHJIKOGMNPQR -row major 3': AGMDHPBINEJQCKOFLR -col major 3': ABCDEFGIKHJLMNOPQR -row major 5' or 3': ADGMPEHIBNQFJKCORL -col major 5' or 3': ABCDEFLHJGIKOMNPQR -row major top-left domain start: ADGMPEHIBNQFJKCORL -col major top-left domain start: ABCDEFLHJGIKMNOPQR - """ - helices = [sc.Helix(max_offset=100) for _ in range(6)] - self.design_6h: sc.Design = sc.Design(helices=helices, strands=[], grid=sc.square) - d = self.design_6h - - d.draw_strand(1, 0).move(16).cross(0).move(-16).with_name('A') - d.draw_strand(3, 0).move(16).cross(2).move(-16).with_name('B') - d.draw_strand(5, 0).move(16).cross(4).move(-16).with_name('C') - - d.draw_strand(0, 40).move(-24).cross(1).move(8).with_name('D') - - d.draw_strand(1, 24).move(8).cross(2).move(-16).cross(3).move(8).with_name('E') - d.draw_strand(3, 24).move(8).cross(4).move(-16).cross(5).move(8).with_name('F') - - d.draw_strand(0, 72).move(-32).with_name('G') - - d.draw_strand(2, 40).move(-8).cross(1).move(24).with_name('H') - d.draw_strand(1, 56).move(8).cross(2).move(-24).with_name('I') - - d.draw_strand(4, 40).move(-8).cross(3).move(24).with_name('J') - d.draw_strand(3, 56).move(8).cross(4).move(-24).with_name('K') - - d.draw_strand(5, 24).move(32).with_name('L') - - d.draw_strand(2, 72).move(-8).cross(1).move(16).cross(0).move(-8).with_name('M') - d.draw_strand(4, 72).move(-8).cross(3).move(16).cross(2).move(-8).with_name('N') - - d.draw_strand(5, 56).move(24).cross(4).move(-8).with_name('O') - - d.draw_strand(0, 96).move(-16).cross(1).move(16).with_name('P') - d.draw_strand(2, 96).move(-16).cross(3).move(16).with_name('Q') - d.draw_strand(4, 96).move(-16).cross(5).move(16).with_name('R') - - for strand in d.strands: - d.assign_dna(strand, 'A' * 32, assign_complement=False) - - @staticmethod - def _get_names_idt(design: sc.Design, key: sc.KeyFunction[sc.Strand]) -> str: - # call design.to_idt_bulk_input_format with given key functions, - # get IDT names of strands exported, and return them joined into a single string - idt_str = design.to_idt_bulk_input_format(key=key) - idt_lines = idt_str.split('\n') - names = [] - for line in idt_lines: - name = line.split(',')[0] - names.append(name) - names_joined = ''.join(names) - return names_joined - - def test_to_idt_bulk_input_format__row_major_5p(self) -> None: - key = sc.strand_order_key_function(column_major=False, strand_order=sc.StrandOrder.five_prime) - names_joined = self._get_names_idt(self.design_6h, key) - self.assertEqual('DGPAEIHMQBFKJNRCLO', names_joined) - - def test_to_idt_bulk_input_format__col_major_5p(self) -> None: - key = sc.strand_order_key_function(column_major=True, strand_order=sc.StrandOrder.five_prime) - names_joined = self._get_names_idt(self.design_6h, key) - self.assertEqual('ABCEFLDHJIKOGMNPQR', names_joined) - - def test_to_idt_bulk_input_format__row_major_3p(self) -> None: - key = sc.strand_order_key_function(column_major=False, strand_order=sc.StrandOrder.three_prime) - names_joined = self._get_names_idt(self.design_6h, key) - self.assertEqual('AGMDHPBINEJQCKOFLR', names_joined) - - def test_to_idt_bulk_input_format__col_major_3p(self) -> None: - key = sc.strand_order_key_function(column_major=True, strand_order=sc.StrandOrder.three_prime) - names_joined = self._get_names_idt(self.design_6h, key) - self.assertEqual('ABCDEFGIKHJLMNOPQR', names_joined) - - def test_to_idt_bulk_input_format__row_major_5p_or_3p(self) -> None: - key = sc.strand_order_key_function(column_major=False, - strand_order=sc.StrandOrder.five_or_three_prime) - names_joined = self._get_names_idt(self.design_6h, key) - self.assertEqual('ADGMPEHIBNQFJKCORL', names_joined) - - def test_to_idt_bulk_input_format__col_major_5p_or_3p(self) -> None: - key = sc.strand_order_key_function(column_major=True, strand_order=sc.StrandOrder.five_or_three_prime) - names_joined = self._get_names_idt(self.design_6h, key) - self.assertEqual('ABCDEFLHJGIKOMNPQR', names_joined) - - def test_to_idt_bulk_input_format__row_major_top_left_domain_start(self) -> None: - key = sc.strand_order_key_function(column_major=False, strand_order=sc.StrandOrder.top_left_domain) - names_joined = self._get_names_idt(self.design_6h, key) - self.assertEqual('ADGMPEHIBNQFJKCORL', names_joined) - - def test_to_idt_bulk_input_format__col_major_top_left_domain_start(self) -> None: - key = sc.strand_order_key_function(column_major=True, strand_order=sc.StrandOrder.top_left_domain) - names_joined = self._get_names_idt(self.design_6h, key) - self.assertEqual('ABCDEFLHJGIKMNOPQR', names_joined) - - def test_to_idt_bulk_input_format__duplicate_names_same_sequence(self) -> None: - length = 8 - helices = [sc.Helix(max_offset=length)] - ss1_r = sc.Domain(0, True, 0, 4) - ss2_r = sc.Domain(0, True, 4, 8) - ss_l = sc.Domain(0, False, 0, 4) - - s1_r = sc.Strand([ss1_r], idt=sc.IDTFields(), name='s1_r') - s2_r = sc.Strand([ss2_r], idt=sc.IDTFields(), name='s1_r') - s_l = sc.Strand([ss_l], idt=sc.IDTFields(), name='s_l') - - strands = [s1_r, s2_r, s_l] - - design = sc.Design(helices=helices, strands=strands, grid=sc.square) - - design.assign_dna(s_l, 'AGTT') - design.assign_dna(s2_r, 'AACT') - - # should not raise exception - design.to_idt_bulk_input_format() - - def test_to_idt_bulk_input_format__duplicate_names_different_sequences(self) -> None: - ss1_r = sc.Domain(0, True, 0, 4) - ss2_r = sc.Domain(0, True, 4, 8) - ss_l = sc.Domain(0, False, 0, 4) - - s1_r = sc.Strand([ss1_r], idt=sc.IDTFields(), name='s1_r') - s2_r = sc.Strand([ss2_r], idt=sc.IDTFields(), name='s1_r') - s_l = sc.Strand([ss_l], idt=sc.IDTFields(), name='s_l') - - strands = [s1_r, s2_r, s_l] - - design = sc.Design(strands=strands, grid=sc.square) - - design.assign_dna(s_l, 'AGTT') - design.assign_dna(s2_r, 'GGGG') - - with self.assertRaises(sc.IllegalDesignError): - design.to_idt_bulk_input_format() - - def test_to_idt_bulk_input_format__duplicate_names_different_scales(self) -> None: - ss1_r = sc.Domain(0, True, 0, 4) - ss2_r = sc.Domain(0, True, 4, 8) - ss_l = sc.Domain(0, False, 0, 4) - - s1_r = sc.Strand([ss1_r], idt=sc.IDTFields(scale='25nm'), name='s1_r') - s2_r = sc.Strand([ss2_r], idt=sc.IDTFields(scale='100nm'), name='s1_r') - s_l = sc.Strand([ss_l], idt=sc.IDTFields(), name='s_l') - - strands = [s1_r, s2_r, s_l] - - design = sc.Design(strands=strands, grid=sc.square) - - design.assign_dna(s_l, 'AGTT') - design.assign_dna(s2_r, 'AACT') - - with self.assertRaises(sc.IllegalDesignError): - design.to_idt_bulk_input_format() - - # https://stackoverflow.com/a/46307456/5339430 - # @mock.patch('sys.stdout', new_callable=io.StringIO) - def test_to_idt_bulk_input_format__duplicate_names_different_purifications(self) -> None: - ss1_r = sc.Domain(0, True, 0, 4) - ss2_r = sc.Domain(0, True, 4, 8) - ss_l = sc.Domain(0, False, 0, 4) - - s1_r = sc.Strand([ss1_r], idt=sc.IDTFields(purification='STD'), name='s1_r') - s2_r = sc.Strand([ss2_r], idt=sc.IDTFields(purification='HPLC'), name='s1_r') - s_l = sc.Strand([ss_l], idt=sc.IDTFields(), name='s_l') - - strands = [s1_r, s2_r, s_l] - - design = sc.Design(strands=strands, grid=sc.square) - - design.assign_dna(s_l, 'AGTT') - design.assign_dna(s2_r, 'AACT') - - with self.assertRaises(sc.IllegalDesignError): - design.to_idt_bulk_input_format(warn_duplicate_name=True) - # printed = mock_print.getvalue() - # self.assertIn('two strands with same IDT name', printed) - # self.assertIn('s1_r', printed) - - def test_write_idt_plate_excel_file(self) -> None: - strand_len = 10 - - # add 10 strands in excess of 3 plates - for plate_type in [sc.PlateType.wells96, sc.PlateType.wells384]: - num_strands = 3 * plate_type.num_wells_per_plate() + 10 - filename = f'test_excel_export_{plate_type.num_wells_per_plate()}.xls' - max_offset = num_strands * strand_len - helices = [sc.Helix(max_offset=max_offset) for _ in range(1)] - design = sc.Design(helices=helices, strands=[], grid=sc.square) - for strand_idx in range(num_strands): - design.draw_strand(0, strand_len * strand_idx).move(strand_len).with_name(f's{strand_idx}') - design.strands[-1].set_dna_sequence('T' * strand_len) - - design.write_idt_plate_excel_file(filename=filename, plate_type=plate_type) - - book = xlrd.open_workbook(filename) - self.assertEqual(4, book.nsheets) - for plate in range(4): - sheet = book.sheet_by_index(plate) - self.assertEqual(3, sheet.ncols) - - if plate == 2: # penultimate plate - expected_wells = plate_type.num_wells_per_plate() - plate_type.min_wells_per_plate() + 10 - elif plate == 3: # last plate - expected_wells = plate_type.min_wells_per_plate() - else: - expected_wells = plate_type.num_wells_per_plate() - - self.assertEqual(expected_wells + 1, sheet.nrows) - - os.remove(filename) - - -class TestExportCadnanoV2(unittest.TestCase): - """ - Tests the export feature to cadnano v2 (see misc/cadnano-format-specs/v2.txt). - """ - folder = "cadnano_v2_export" - input_path = os.path.join('tests_inputs', folder) - # This variable is used for writing out files, which can be useful for debugging - output_path = os.path.join('tests_outputs', folder) - ext = sc.default_scadnano_file_extension - - def test_export_design_with_helix_group(self): - e = 'east' - s = 'south' - helices = [ - sc.Helix(max_offset=24, group=s), - sc.Helix(max_offset=25, group=s), - ] - helices.extend([ - sc.Helix(max_offset=22, group=e), - sc.Helix(max_offset=23, group=e), - ]) - - group_south = sc.HelixGroup(position=sc.Position3D(x=0, y=10, z=0), - grid=sc.square) - group_east = sc.HelixGroup(position=sc.Position3D(x=10, y=0, z=0), grid=sc.square) - - groups = { - e: group_east, - s: group_south - } - - design = sc.Design(helices=helices, groups=groups, strands=[]) - output_json = design.to_cadnano_v2_json() - - output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) - self.assertEqual(4, len(output_design.helices)) - - # To help with debugging, uncomment these lines to write out the - # scadnano and/or cadnano file - # - # design.write_scadnano_file(directory=self.input_path, - # filename=f'test_export_design_with_helix_group.{self.ext}') - # design.write_cadnano_v2_file(directory=self.output_path, - # filename='test_export_design_with_helix_group.json') - - def test_export_design_with_helix_group_not_same_grid(self): - e = 'east' - s = 'south' - helices = [ - sc.Helix(max_offset=24, group=s), - sc.Helix(max_offset=25, group=s), - ] - helices.extend([ - sc.Helix(max_offset=22, group=e), - sc.Helix(max_offset=23, group=e), - ]) - - group_south = sc.HelixGroup(position=sc.Position3D(x=0, y=10, z=0), - grid=sc.square) - group_east = sc.HelixGroup(position=sc.Position3D(x=10, y=0, z=0), grid=sc.honeycomb) - - groups = { - e: group_east, - s: group_south - } - - design = sc.Design(helices=helices, groups=groups, strands=[]) - - # To help with debugging, uncomment these lines to write out the - # scadnano file - # - # design.write_scadnano_file(directory=self.input_path, - # filename=f'test_export_design_with_helix_group_not_same_grid.{self.ext}') - - with self.assertRaises(ValueError) as context: - design.to_cadnano_v2_json() - # design.write_cadnano_v2_file(directory=self.output_path, - # filename='test_export_design_with_helix_group_not_same_grid.json') - self.assertTrue('helix groups' in context.exception.args[0]) - - def test_2_staple_2_helix_origami_extremely_simple(self): - helices = [sc.Helix(max_offset=32), sc.Helix(max_offset=32)] - scaf_part = sc.Domain(helix=0, forward=True, start=0, end=32) - scaf = sc.Strand(domains=[scaf_part], is_scaffold=True) - design = sc.Design(helices=helices, strands=[scaf], grid=sc.square) - output_json = design.to_cadnano_v2_json() - - output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) - self.assertEqual(2, len(output_design.helices)) - # To help with debugging, uncomment these lines to write out the - # scadnano and/or cadnano file - # - # design.write_scadnano_file(directory=self.input_path, - # filename=f'test_2_stape_2_helix_origami_extremely_simple.{self.ext}') - # design.write_cadnano_v2_file(directory=self.output_path, - # filename='test_2_stape_2_helix_origami_extremely_simple.json') - - def test_2_staple_2_helix_origami_extremely_simple_2(self) -> None: - helices = [sc.Helix(max_offset=32), sc.Helix(max_offset=32)] - scaf_part1 = sc.Domain(helix=0, forward=True, start=0, end=32) - scaf_part2 = sc.Domain(helix=1, forward=False, start=0, end=32) - scaf = sc.Strand(domains=[scaf_part1, scaf_part2], is_scaffold=True) - design = sc.Design(helices=helices, strands=[scaf], grid=sc.square) - - # To help with debugging, uncomment these lines to write out the - # scadnano and/or cadnano file - # - # design.write_scadnano_file(directory=self.input_path, - # filename=f'test_2_stape_2_helix_origami_extremely_simple_2.{self.ext}') - # design.write_cadnano_v2_file(directory=self.output_path, - # filename='test_2_stape_2_helix_origami_extremely_simple_2.json') - output_json = design.to_cadnano_v2_json() - - output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) - self.assertEqual(2, len(output_design.helices)) - - def test_2_staple_2_helix_origami_deletions_insertions(self) -> None: - # left staple - stap_left_ss1 = sc.Domain(helix=1, forward=True, start=0, end=16) - stap_left_ss0 = sc.Domain(helix=0, forward=False, start=0, end=16) - stap_left = sc.Strand(domains=[stap_left_ss1, stap_left_ss0]) - - # right staple - stap_right_ss0 = sc.Domain(helix=0, forward=False, start=16, end=32) - stap_right_ss1 = sc.Domain(helix=1, forward=True, start=16, end=32) - stap_right = sc.Strand(domains=[stap_right_ss0, stap_right_ss1]) - - # scaffold - scaf_ss1_left = sc.Domain(helix=1, forward=False, start=0, end=16) - scaf_ss0 = sc.Domain(helix=0, forward=True, start=0, end=32) - # loopout = sc.Loopout(length=3) No loopout in cadnano - scaf_ss1_right = sc.Domain(helix=1, forward=False, start=16, end=32) - scaf = sc.Strand(domains=[scaf_ss1_left, scaf_ss0, scaf_ss1_right], is_scaffold=True) - - # whole design - design = sc.Design(strands=[scaf, stap_left, stap_right], grid=sc.square) - - # deletions and insertions added to design so they can be added to both strands on a helix - design.add_deletion(helix=0, offset=11) - design.add_deletion(helix=0, offset=12) - design.add_deletion(helix=0, offset=24) - design.add_deletion(helix=1, offset=12) - design.add_deletion(helix=1, offset=24) - - design.add_insertion(helix=0, offset=6, length=1) - design.add_insertion(helix=0, offset=18, length=2) - design.add_insertion(helix=1, offset=6, length=3) - design.add_insertion(helix=1, offset=18, length=4) - - # also assigns complement to strands other than scaf bound to it - output_json = design.to_cadnano_v2_json() - - output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) - # To help with debugging, uncomment these lines to write out the - self.assertEqual(output_design.grid, sc.Grid.square) - self.assertEqual(2, len(output_design.helices)) - output_helix_0 = output_design.helices[0] - output_helix_1 = output_design.helices[1] - self.assertEqual(output_helix_0.grid_position, (0, 0)) - self.assertEqual(output_helix_1.grid_position, (0, 1)) - self.assertEqual(3, len(output_design.strands)) - self.assertIn(stap_left, output_design.strands) - self.assertIn(stap_right, output_design.strands) - self.assertIn(scaf, output_design.strands) - # scadnano and/or cadnano file - # - # design.write_scadnano_file(directory=self.input_path, - # filename=f'test_2_stape_2_helix_origami_deletions_insertions.{self.ext}') - # design.write_cadnano_v2_file(directory=self.output_path, - # filename='test_2_stape_2_helix_origami_deletions_insertions.json') - - def test_6_helix_origami_rectangle(self) -> None: - design = rect.create(num_helices=6, num_cols=10, nick_pattern=rect.staggered, - twist_correction_deletion_spacing=3) - output_json = design.to_cadnano_v2_json() - - output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) - # To help with debugging, uncomment these lines to write out the - self.assertEqual(6, len(output_design.helices)) - - # scadnano and/or cadnano file - # - # design.write_scadnano_file(directory=self.input_path, - # filename=f'test_6_helix_origami_rectangle.{self.ext}') - # design.write_cadnano_v2_file(directory=self.output_path, - # filename='test_6_helix_origami_rectangle.json') - - def test_export_no_whitespace(self) -> None: - design = rect.create(num_helices=6, num_cols=10, nick_pattern=rect.staggered, - twist_correction_deletion_spacing=3) - output_json_with_space = design.to_cadnano_v2_json(whitespace=True) - self.assertIn(' ', output_json_with_space) - self.assertIn('\n', output_json_with_space) - output_json_no_space = design.to_cadnano_v2_json(whitespace=False) - self.assertNotIn(' ', output_json_no_space) - self.assertNotIn('\n', output_json_no_space) - - # scadnano and/or cadnano file - # - # design.write_scadnano_file(directory=self.input_path, - # filename=f'test_6_helix_origami_rectangle.{self.ext}') - # design.write_cadnano_v2_file(directory=self.output_path, - # filename='test_6_helix_origami_rectangle.json') - - def test_6_helix_bundle_honeycomb(self) -> None: - design = sc.Design.from_scadnano_file( - os.path.join(self.input_path, f'test_6_helix_bundle_honeycomb.{self.ext}')) - output_json = design.to_cadnano_v2_json() - - output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) - # To help with debugging, uncomment these lines to write out the - self.assertEqual(6, len(output_design.helices)) - # cadnano file - # - # design.write_cadnano_v2_file(directory=self.output_path, - # filename='test_6_helix_bundle_honeycomb.json') - - def test_16_helix_origami_rectangle_no_twist(self) -> None: - design = rect.create(num_helices=16, num_cols=26, assign_seq=True, - twist_correction_deletion_spacing=3) - # To help with debugging, uncomment these lines to write out the - # scadnano and/or cadnano file - # - # design.write_scadnano_file(directory=self.input_path, - # filename=f'test_16_helix_origami_rectangle_no_twist.{self.ext}') - # design.write_cadnano_v2_file(directory=self.output_path, - # filename='test_16_helix_origami_rectangle_no_twist.json') - output_json = design.to_cadnano_v2_json() - - output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) - self.assertEqual(16, len(output_design.helices)) - - def test_circular_strand(self) -> None: - helices = [sc.Helix(max_offset=24) for _ in range(2)] - design = sc.Design(helices=helices, grid=sc.square) - - design.draw_strand(1, 0).move(8).cross(0).move(-8).as_circular() - # To help with debugging, uncomment these lines to write out the - # scadnano and/or cadnano file - # - # design.write_scadnano_file(directory=self.input_path, - # filename=f'test_circular_strand.{self.ext}') - # design.write_cadnano_v2_file(directory=self.output_path, - # filename='test_circular_strand.json') - output_json = design.to_cadnano_v2_json() - - output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) - self.assertEqual(2, len(output_design.helices)) - - def test_big_circular_staples_hex(self) -> None: - design = sc.Design.from_scadnano_file( - os.path.join(self.input_path, f'test_big_circular_staples_hex.{self.ext}')) - # To help with debugging, uncomment these lines to write out the - # scadnano and/or cadnano file - # - # design.write_cadnano_v2_file(directory=self.output_path, - # filename='test_big_circular_staples_hex.json') - output_json = design.to_cadnano_v2_json() - - output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) - self.assertEqual(6, len(output_design.helices)) - - def test_big_circular_staples(self) -> None: - design = sc.Design.from_scadnano_file( - os.path.join(self.input_path, f'test_big_circular_staples.{self.ext}')) - # To help with debugging, uncomment these lines to write out the - # scadnano and/or cadnano file - # - # design.write_cadnano_v2_file(directory=self.output_path, - # filename='test_big_circular_staples.json') - output_json = design.to_cadnano_v2_json() - - output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) - self.assertEqual(8, len(output_design.helices)) - - def test_paranemic_crossover(self) -> None: - design = sc.Design.from_scadnano_file( - os.path.join(self.input_path, f'test_paranemic_crossover.{self.ext}')) - # To help with debugging, uncomment these lines to write out the - # scadnano and/or cadnano file - # - # design.write_cadnano_v2_file(directory=self.output_path, - # filename='test_paranemic_crossover.json') - output_json = design.to_cadnano_v2_json() - - output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) - self.assertEqual(4, len(output_design.helices)) - - def test_parity_issue(self) -> None: - """ We do not design where the parity of the helix - does not correspond to the direction. - """ - # Bad case one: parity issue in design (see cadnano v2 format spec, v2.txt) - helices = [sc.Helix(max_offset=32), sc.Helix(max_offset=32)] - scaf_part = sc.Domain(helix=1, forward=True, start=0, end=32) - scaf = sc.Strand(domains=[scaf_part], is_scaffold=True) - design = sc.Design(helices=helices, strands=[scaf], grid=sc.square) - - with self.assertRaises(ValueError) as context: - # To help with debugging, uncomment these lines to write out the - # scadnano and/or cadnano file - # - # design.write_cadnano_v2_file(directory=self.output_path, - # filename='test_parity_issue.json') - design.to_cadnano_v2_json() - self.assertTrue('forward' in context.exception.args[0]) - - def test_loopout(self) -> None: - """ We do not handle Loopouts - """ - # Bad case two: Loopouts - helices = [sc.Helix(max_offset=48), sc.Helix(max_offset=48)] - - # left staple - stap_left_ss1 = sc.Domain(helix=1, forward=True, start=8, end=24) - stap_left_ss0 = sc.Domain(helix=0, forward=False, start=8, end=24) - stap_left = sc.Strand(domains=[stap_left_ss1, stap_left_ss0]) - - # right staple - stap_right_ss0 = sc.Domain(helix=0, forward=False, start=24, end=40) - stap_right_ss1 = sc.Domain(helix=1, forward=True, start=24, end=40) - stap_right = sc.Strand(domains=[stap_right_ss0, stap_right_ss1]) - - # scaffold - scaf_ss1_left = sc.Domain(helix=1, forward=False, start=8, end=24) - scaf_ss0 = sc.Domain(helix=0, forward=True, start=8, end=40) - loopout = sc.Loopout(length=3) - scaf_ss1_right = sc.Domain(helix=1, forward=False, start=24, end=40) - scaf = sc.Strand(domains=[scaf_ss1_left, scaf_ss0, loopout, scaf_ss1_right], is_scaffold=True) - - # whole design - design = sc.Design(helices=helices, strands=[scaf, stap_left, stap_right], grid=sc.square) - - # deletions and insertions added to design are added to both strands on a helix - design.add_deletion(helix=1, offset=20) - design.add_insertion(helix=0, offset=14, length=1) - design.add_insertion(helix=0, offset=26, length=2) - - with self.assertRaises(ValueError) as context: - # To help with debugging, uncomment these lines to write out the - # scadnano and/or cadnano file - # - # design.write_cadnano_v2_file(directory=self.output_path, - # filename='test_loopout_issue.json') - design.to_cadnano_v2_json() - self.assertTrue('Loopouts' in context.exception.args[0]) - - def test_extension(self) -> None: - """ We do not handle Extensions - """ - design: sc.Design = sc.Design(helices=[sc.Helix(max_offset=100)], grid=sc.Grid.square) - sb = design.draw_strand(0, 0) - sb.to(10) - sb.as_scaffold() - sb.extension_3p(5) - - with self.assertRaises(ValueError) as context: - design.to_cadnano_v2_json() - self.assertTrue('Extensions' in context.exception.args[0]) - - -class TestDesignFromJson(unittest.TestCase): - """ - Tests reading a design from a dict derived from JSON. - """ - - def setUp(self) -> None: - r""" - 0 8 16 - | | | - 0 +--X-----------+ - /<--X---++------]\ - | || loopout(3) - \[---2--++------>/ - 1 +---2--]<------+ - """ - st_l = sc.Strand([ - sc.Domain(1, True, 0, 8, insertions=[(4, 2)]), - sc.Domain(0, False, 0, 8, deletions=[3]), - ], modification_5p=mod.biotin_5p) - st_r = sc.Strand([ - sc.Domain(0, False, 8, 16), - sc.Domain(1, True, 8, 16), - ], modification_5p=mod.biotin_5p, modification_3p=mod.cy3_3p, modifications_int={ - 1: mod.biotin_int, 2: mod.cy5_int - }) - scaf = sc.Strand([ - sc.Domain(1, False, 0, 8, insertions=[(4, 2)]), - sc.Domain(0, True, 0, 16, deletions=[3]), - sc.Loopout(3), - sc.Domain(1, False, 8, 16, deletions=[]), - ], is_scaffold=True) - - self.design_pre_json = sc.Design(strands=[st_l, st_r, scaf], grid=sc.square) - self.design_pre_json.assign_dna(scaf, 'A' * 36) - - def test_from_json__from_and_to_file_contents(self) -> None: - json_str = self.design_pre_json.to_json() - json_map = json.loads(json_str) - design = sc.Design.from_scadnano_json_map(json_map) - design.to_json_serializable() - - def test_from_json__three_strands(self) -> None: - json_str = self.design_pre_json.to_json() - json_map = json.loads(json_str) - design = sc.Design.from_scadnano_json_map(json_map) - - self.assertTrue(isinstance(design, sc.Design)) - - self.assertEqual(sc.Grid.square, design.grid) - - self.assertEqual(2, len(design.helices)) - helix0 = design.helices[0] - helix1 = design.helices[1] - self.assertEqual(0, helix0.idx) - self.assertEqual(0, helix0.min_offset) - self.assertEqual(16, helix0.max_offset) - self.assertEqual((0, 0), helix0.grid_position) - self.assertEqual(1, helix1.idx) - self.assertEqual(0, helix1.min_offset) - self.assertEqual(16, helix1.max_offset) - self.assertEqual((0, 1), helix1.grid_position) - - self.assertEqual(3, len(design.strands)) - st_l = design.strands[0] - st_r = design.strands[1] - scaf = design.strands[2] - - self.assertEqual(scaf, design.scaffold) - - self.assertEqual(2, len(st_l.domains)) - self.assertEqual(2, len(st_r.domains)) - self.assertEqual(4, len(scaf.domains)) - - self.assertEqual('A' * 36, scaf.dna_sequence) - self.assertEqual('T' * 17, st_l.dna_sequence) - self.assertEqual('T' * 16, st_r.dna_sequence) - - st_l_ss0 = st_l.domains[0] - st_l_ss1 = st_l.domains[1] - st_r_ss0 = st_r.domains[0] - st_r_ss1 = st_r.domains[1] - scaf_ss0 = scaf.domains[0] - scaf_ss1 = scaf.domains[1] - scaf_loop = scaf.domains[2] - scaf_ss2 = scaf.domains[3] - - self.assertEqual(3, scaf_loop.length) - - self.assertEqual(1, st_l_ss0.helix) - self.assertEqual(0, st_l_ss1.helix) - self.assertEqual(0, st_r_ss0.helix) - self.assertEqual(1, st_r_ss1.helix) - self.assertEqual(1, scaf_ss0.helix) - self.assertEqual(0, scaf_ss1.helix) - self.assertEqual(1, scaf_ss2.helix) - - self.assertEqual(True, st_l_ss0.forward) - self.assertEqual(False, st_l_ss1.forward) - self.assertEqual(False, st_r_ss0.forward) - self.assertEqual(True, st_r_ss1.forward) - self.assertEqual(False, scaf_ss0.forward) - self.assertEqual(True, scaf_ss1.forward) - self.assertEqual(False, scaf_ss2.forward) - - self.assertEqual(0, st_l_ss0.start) - self.assertEqual(8, st_l_ss0.end) - self.assertEqual(0, st_l_ss1.start) - self.assertEqual(8, st_l_ss1.end) - self.assertEqual(8, st_r_ss0.start) - self.assertEqual(16, st_r_ss0.end) - self.assertEqual(8, st_r_ss1.start) - self.assertEqual(16, st_r_ss1.end) - self.assertEqual(0, scaf_ss0.start) - self.assertEqual(8, scaf_ss0.end) - self.assertEqual(0, scaf_ss1.start) - self.assertEqual(16, scaf_ss1.end) - self.assertEqual(8, scaf_ss2.start) - self.assertEqual(16, scaf_ss2.end) - - self.assertListEqual([(4, 2)], st_l_ss0.insertions) - self.assertListEqual([], st_l_ss0.deletions) - self.assertListEqual([], st_l_ss1.insertions) - self.assertListEqual([3], st_l_ss1.deletions) - self.assertListEqual([], st_r_ss0.insertions) - self.assertListEqual([], st_r_ss0.deletions) - self.assertListEqual([], st_r_ss1.insertions) - self.assertListEqual([], st_r_ss1.deletions) - self.assertListEqual([(4, 2)], scaf_ss0.insertions) - self.assertListEqual([], scaf_ss0.deletions) - self.assertListEqual([], scaf_ss1.insertions) - self.assertListEqual([3], scaf_ss1.deletions) - self.assertListEqual([], scaf_ss2.insertions) - self.assertListEqual([], scaf_ss2.deletions) - - self.assertEqual(mod.biotin_5p, st_l.modification_5p) - self.assertEqual(None, st_l.modification_3p) - self.assertDictEqual({}, st_l.modifications_int) - - self.assertEqual(mod.biotin_5p, st_r.modification_5p) - self.assertEqual(mod.cy3_3p, st_r.modification_3p) - self.assertDictEqual({1: mod.biotin_int, 2: mod.cy5_int}, st_r.modifications_int) - - self.assertEqual(None, scaf.modification_5p) - self.assertEqual(None, scaf.modification_3p) - self.assertDictEqual({}, scaf.modifications_int) - - def test_from_json__helices_non_default_indices(self) -> None: - h2 = sc.Helix(idx=2) - h3 = sc.Helix(idx=3) - h5 = sc.Helix(idx=5) - helices = [h2, h3, h5] - s1 = sc.Strand([ - sc.Domain(2, True, 0, 4), - sc.Domain(3, False, 0, 4), - ]) - s2 = sc.Strand([ - sc.Domain(3, True, 4, 8), - sc.Domain(5, False, 4, 8), - ]) - self.design_pre_json = sc.Design(helices=helices, strands=[s1, s2], grid=sc.square) - - json_str = self.design_pre_json.to_json() - json_map = json.loads(json_str) - design = sc.Design.from_scadnano_json_map(json_map) - - self.assertEqual(3, len(design.helices)) - - self.assertEqual(2, design.helices[2].idx) - self.assertEqual(3, design.helices[3].idx) - self.assertEqual(5, design.helices[5].idx) - - def test_from_json__helices_non_default_indices_mixed_with_default(self) -> None: - h2 = sc.Helix(idx=2) - h3 = sc.Helix() - h5 = sc.Helix(idx=5) - helices = [h2, h3, h5] - s1 = sc.Strand([ - sc.Domain(2, True, 0, 4), - sc.Domain(1, False, 0, 4), - ]) - s2 = sc.Strand([ - sc.Domain(1, True, 4, 8), - sc.Domain(5, False, 4, 8), - ]) - self.design_pre_json = sc.Design(helices=helices, strands=[s1, s2], grid=sc.square) - - json_str = self.design_pre_json.to_json() - - json_map = json.loads(json_str) - design = sc.Design.from_scadnano_json_map(json_map) - - self.assertEqual(3, len(design.helices)) - - self.assertEqual(2, design.helices[2].idx) - self.assertEqual(1, design.helices[1].idx) - self.assertEqual(5, design.helices[5].idx) - - def test_from_json__helices_non_default_error_if_some_have_idx_not_others(self) -> None: - h2 = sc.Helix(idx=2) - h3 = sc.Helix(idx=3) - h3_2 = sc.Helix(idx=3) - helices = [h2, h3, h3_2] - with self.assertRaises(sc.IllegalDesignError): - self.design_pre_json = sc.Design(helices=helices, strands=[], grid=sc.square) - - def test_from_json__helices_non_default_error_if_some_have_idx_not_others_mixed_default(self) -> None: - h0 = sc.Helix() - h0_2 = sc.Helix(idx=0) - helices = [h0, h0_2] - with self.assertRaises(sc.IllegalDesignError): - self.design_pre_json = sc.Design(helices=helices, strands=[], grid=sc.square) - - -class TestStrandReversePolarity(unittest.TestCase): - """ - Tests reversing polarity of all strands in the system. - """ - - def test_reverse_all__one_strand(self) -> None: - """ - before - 0 8 - | | - 0 [---X--> - - after - 0 <---X--] - """ - design = sc.Design( - strands=[sc.Strand([sc.Domain(0, True, 0, 8, deletions=[4])])], - grid=sc.square) - design.reverse_all() - - self.assertEqual(1, len(design.strands)) - strand = design.strands[0] - self.assertEqual(1, len(strand.domains)) - self.assertEqual(7, strand.offset_5p()) - self.assertEqual(0, strand.offset_3p()) - ss = strand.domains[0] - self.assertEqual(0, ss.helix) - self.assertEqual(False, ss.forward) - self.assertEqual(0, ss.start) - self.assertEqual(8, ss.end) - - def test_reverse_all__three_strands(self) -> None: - r""" - before - 0 8 16 - | | | - - 0 +--X-----------+ - /<--X---++------]\ - | || | - \[---2--++------>/ - 1 +---2--]<------+ - - after - 0 +--X-----------+ - /[--X---++------>\ - | || | - \<---2--++------]/ - 1 +---2-->[------+ - """ - design = sc.Design( - strands=[ - sc.Strand([ - sc.Domain(1, True, 0, 8, insertions=[(4, 2)]), - sc.Domain(0, False, 0, 8, deletions=[3]), - ]), - sc.Strand([ - sc.Domain(0, False, 8, 16), - sc.Domain(1, True, 8, 16), - ]), - sc.Strand([ - sc.Domain(1, False, 0, 8, insertions=[(4, 2)]), - sc.Domain(0, True, 0, 16, deletions=[3]), - sc.Domain(1, False, 8, 16, deletions=[]), - ]), - ], - grid=sc.square) - design.reverse_all() - - self.assertEqual(3, len(design.strands)) - stap_l = design.strands[0] - stap_r = design.strands[1] - scaf = design.strands[2] - - self.assertEqual(0, stap_l.offset_5p()) - self.assertEqual(0, stap_l.offset_3p()) - self.assertEqual(15, stap_r.offset_5p()) - self.assertEqual(15, stap_r.offset_3p()) - self.assertEqual(8, scaf.offset_5p()) - self.assertEqual(7, scaf.offset_3p()) - - self.assertEqual(2, len(stap_l.domains)) - stap_l_ss0 = stap_l.domains[0] - stap_l_ss1 = stap_l.domains[1] - - self.assertEqual(0, stap_l_ss0.helix) - self.assertEqual(True, stap_l_ss0.forward) - self.assertEqual(0, stap_l_ss0.start) - self.assertEqual(8, stap_l_ss0.end) - self.assertEqual([3], stap_l_ss0.deletions) - self.assertEqual([], stap_l_ss0.insertions) - - self.assertEqual(1, stap_l_ss1.helix) - self.assertEqual(False, stap_l_ss1.forward) - self.assertEqual(0, stap_l_ss1.start) - self.assertEqual(8, stap_l_ss1.end) - self.assertEqual([], stap_l_ss1.deletions) - self.assertEqual([(4, 2)], stap_l_ss1.insertions) - - self.assertEqual(2, len(stap_r.domains)) - stap_r_ss0 = stap_r.domains[0] - stap_r_ss1 = stap_r.domains[1] - - self.assertEqual(1, stap_r_ss0.helix) - self.assertEqual(False, stap_r_ss0.forward) - self.assertEqual(8, stap_r_ss0.start) - self.assertEqual(16, stap_r_ss0.end) - self.assertEqual([], stap_r_ss0.deletions) - self.assertEqual([], stap_r_ss0.insertions) - - self.assertEqual(0, stap_r_ss1.helix) - self.assertEqual(True, stap_r_ss1.forward) - self.assertEqual(8, stap_r_ss1.start) - self.assertEqual(16, stap_r_ss1.end) - self.assertEqual([], stap_r_ss1.deletions) - self.assertEqual([], stap_r_ss1.insertions) - - self.assertEqual(3, len(scaf.domains)) - scaf_ss0 = scaf.domains[0] - scaf_ss1 = scaf.domains[1] - scaf_ss2 = scaf.domains[2] - - self.assertEqual(1, scaf_ss0.helix) - self.assertEqual(True, scaf_ss0.forward) - self.assertEqual(8, scaf_ss0.start) - self.assertEqual(16, scaf_ss0.end) - self.assertEqual([], scaf_ss0.deletions) - self.assertEqual([], scaf_ss0.insertions) - - self.assertEqual(0, scaf_ss1.helix) - self.assertEqual(False, scaf_ss1.forward) - self.assertEqual(0, scaf_ss1.start) - self.assertEqual(16, scaf_ss1.end) - self.assertEqual([3], scaf_ss1.deletions) - self.assertEqual([], scaf_ss1.insertions) - - self.assertEqual(1, scaf_ss2.helix) - self.assertEqual(True, scaf_ss2.forward) - self.assertEqual(0, scaf_ss2.start) - self.assertEqual(8, scaf_ss2.end) - self.assertEqual([], scaf_ss2.deletions) - self.assertEqual([(4, 2)], scaf_ss2.insertions) - - -class TestInlineInsDel(unittest.TestCase): - """ - Tests inlining of insertions/deletions. - """ - - def setUp(self) -> None: - self.design = sc.Design( - helices=[sc.Helix(max_offset=24, major_tick_distance=8)], - strands=[], - grid=sc.square) - - def test_no_deletion_after_loopout(self) -> None: - # not really a test of inlining, but I added the with_deletions and with_insertions to help these - # tests, so easier just to test this behavior here - with self.assertRaises(ValueError): - self.design.draw_strand(0, 0).move(8).loopout(0, 5, 10).with_deletions(4) - - def test_no_insertion_after_loopout(self) -> None: - # not really a test of inlining, but I added the with_deletions and with_insertions to help these - # tests, so easier just to test this behavior here - with self.assertRaises(ValueError): - self.design.draw_strand(0, 0).move(8).loopout(0, 5, 10).with_insertions((4, 2)) - - def test_deletion_below_range(self) -> None: - with self.assertRaises(ValueError): - self.design.draw_strand(0, 4).move(4).with_deletions(2) - - def test_deletion_above_range(self) -> None: - with self.assertRaises(ValueError): - self.design.draw_strand(0, 0).move(4).with_deletions(6) - - def test_insertion_below_range(self) -> None: - with self.assertRaises(ValueError): - self.design.draw_strand(0, 4).move(4).with_insertions((2, 1)) - - def test_insertion_above_range(self) -> None: - with self.assertRaises(ValueError): - self.design.draw_strand(0, 0).move(4).with_insertions((6, 1)) - - def test_inline_deletions_insertions__one_deletion(self) -> None: - """ - before - 0 4 8 16 24 - | | | | - 0 [---X--> - - after - 0 4 7 15 23 - | | | | - 0 [-----> - """ - design = self.design - design.draw_strand(0, 0).move(8).with_deletions(4) - design.inline_deletions_insertions() - self.assert_helix0_strand0_inlined(design, max_offset=23, major_ticks=[0, 7, 15, 23], start=0, end=7) - - def assert_helix0_strand0_inlined(self, design, max_offset, major_ticks, start, end): - self.assertEqual(1, len(design.helices)) - self.assertEqual(1, len(design.strands)) - helix = design.helices[0] - strand = design.strands[0] - self.assertEqual(max_offset, helix.max_offset) - self.assertEqual(major_ticks, helix.major_ticks) - self.assertEqual(start, strand.domains[0].start) - self.assertEqual(end, strand.domains[0].end) - self.assertListEqual([], strand.domains[0].deletions) - self.assertListEqual([], strand.domains[0].insertions) - - def test_inline_deletions_insertions__two_deletions(self) -> None: - """ - before - 0 2 4 8 16 24 - | | | | - 0 [-X-X--> - - after - 0 6 14 22 - | | | | - 0 [----> - """ - design = self.design - design.draw_strand(0, 0).move(8).with_deletions([2, 4]) - design.inline_deletions_insertions() - self.assert_helix0_strand0_inlined(design, max_offset=22, major_ticks=[0, 6, 14, 22], start=0, end=6) - - def test_inline_deletions_insertions__one_insertion(self) -> None: - """ - before - 0 4 8 16 24 - | | | | - 0 [---1--> - - after - 0 9 17 25 - | | | | - 0 [-------> - """ - design = self.design - design.draw_strand(0, 0).move(8).with_insertions((4, 1)) - design.inline_deletions_insertions() - self.assert_helix0_strand0_inlined(design, max_offset=25, major_ticks=[0, 9, 17, 25], start=0, end=9) - - def test_inline_deletions_insertions__two_insertions(self) -> None: - """ - before - 0 2 4 8 16 24 - | | | | - 0 [-3-1--> - - after - 0 12 20 28 - | | | | - 0 [----------> - """ - design = self.design - design.draw_strand(0, 0).move(8).with_insertions([(2, 3), (4, 1)]) - design.inline_deletions_insertions() - self.assert_helix0_strand0_inlined(design, max_offset=28, major_ticks=[0, 12, 20, 28], start=0, - end=12) - - def test_inline_deletions_insertions__one_deletion_one_insertion(self) -> None: - """ - before - 0 2 4 8 16 24 - | | | | - 0 [-3-X--> - - after - 0 10 18 26 - | | | | - 0 [--------> - """ - design = self.design - design.draw_strand(0, 0).move(8).with_deletions(4).with_insertions((2, 3)) - design.inline_deletions_insertions() - self.assert_helix0_strand0_inlined(design, max_offset=26, major_ticks=[0, 10, 18, 26], start=0, - end=10) - - def test_inline_deletions_insertions__one_deletion_right_of_major_tick(self) -> None: - """ - before - 0 89 16 24 - | | | | - 0 [--------X-> - - after - 0 8 15 23 - | | | | - 0 [---------> - """ - design = self.design - design.draw_strand(0, 0).move(12).with_deletions(9) - design.inline_deletions_insertions() - self.assert_helix0_strand0_inlined(design, max_offset=23, major_ticks=[0, 8, 15, 23], start=0, end=11) - - def test_inline_deletions_insertions__one_deletion_on_major_tick(self) -> None: - """ - | is major tick, and . is minor tick - before - 0 8 16 24 - | . . . . . . . | . . . . . . . | . . . . . . . | - [ - - - - - - - X - - > - - after - 0 8 15 23 - | . . . . . . . | . . . . . . | . . . . . . . | - [ - - - - - - - - - > - """ - design = self.design - design.draw_strand(0, 0).move(12).with_deletions(8) - design.inline_deletions_insertions() - self.assert_helix0_strand0_inlined(design, max_offset=23, major_ticks=[0, 8, 15, 23], start=0, end=11) - - def test_inline_deletions_insertions__one_deletion_left_of_major_tick(self) -> None: - """ - before - 0 78 16 24 - | | | | - 0 [------X---> - - after - 0 8 15 23 - | | | | - 0 [---------> - """ - design = self.design - design.draw_strand(0, 0).move(12).with_deletions(7) - design.inline_deletions_insertions() - self.assert_helix0_strand0_inlined(design, max_offset=23, major_ticks=[0, 7, 15, 23], start=0, end=11) - - def test_inline_deletions_insertions__one_insertion_right_of_major_tick(self) -> None: - """ - before - 0 89 16 24 - | | | | - 0 [--------1-> - - after - 0 8 17 25 - | | | | - 0 [-----------> - """ - design = self.design - design.draw_strand(0, 0).move(12).with_insertions((9, 1)) - design.inline_deletions_insertions() - self.assert_helix0_strand0_inlined(design, max_offset=25, major_ticks=[0, 8, 17, 25], start=0, end=13) - - def test_inline_deletions_insertions__one_insertion_on_major_tick(self) -> None: - """ - before - 0 8 16 24 - | | | | - 0 [-------1--> - - after - 0 8 17 25 - | | | | - 0 [-----------> - """ - design = self.design - design.draw_strand(0, 0).move(12).with_insertions((8, 1)) - design.inline_deletions_insertions() - self.assert_helix0_strand0_inlined(design, max_offset=25, major_ticks=[0, 8, 17, 25], start=0, end=13) - - def test_inline_deletions_insertions__one_insertion_left_of_major_tick(self) -> None: - """ - before - 0 78 16 24 - | | | | - 0 [------1---> - - after - 0 9 17 25 - | | | | - 0 [-----------> - """ - design = self.design - design.draw_strand(0, 0).move(12).with_insertions((7, 1)) - design.inline_deletions_insertions() - self.assert_helix0_strand0_inlined(design, max_offset=25, major_ticks=[0, 9, 17, 25], start=0, end=13) - - def test_inline_deletions_insertions__deletions_insertions_in_multiple_domains(self) -> None: - """ - before - 0 5 8 11 16 19 24 - | | | | - 0 [----2-----1-------X----> - - after - 0 10 19 26 - | | | | - 0 [-------------------------> - """ - design = self.design - design.draw_strand(0, 0).move(24).with_deletions(19).with_insertions([(5, 2), (11, 1)]) - design.inline_deletions_insertions() - self.assert_helix0_strand0_inlined(design, max_offset=26, major_ticks=[0, 10, 19, 26], start=0, - end=26) - - def test_inline_deletions_insertions__deletions_insertions_in_multiple_domains_two_strands(self) -> None: - """ - | is major tick, . is minor tick - before - 0 2 5 8 10 16 19 24 - | . . . . . . . | . . . . . . . | . . . . . . . | - [ - X - - 2 - - - - 1 - - > [ - - - - X - - - > - - after - 0 2 5 9 16 18 25 - | . . . . . . . . | . . . . . . . . | . . . . . . | - [ - - - - - - - - - - - - - - > [ - - - - - - - > - """ - design = self.design - design.draw_strand(0, 0).move(14).with_deletions(2).with_insertions([(5, 2), (10, 1)]) - design.draw_strand(0, 14).to(24).with_deletions(19) - design.inline_deletions_insertions() - self.assertEqual(1, len(design.helices)) - self.assertEqual(2, len(design.strands)) - helix = design.helices[0] - strand0 = design.strands[0] - strand1 = design.strands[1] - self.assertEqual(25, helix.max_offset) - self.assertEqual([0, 9, 18, 25], helix.major_ticks) - self.assertEqual(0, strand0.domains[0].start) - self.assertEqual(16, strand0.domains[0].end) - self.assertEqual(16, strand1.domains[0].start) - self.assertEqual(25, strand1.domains[0].end) - self.assertListEqual([], strand0.domains[0].deletions) - self.assertListEqual([], strand0.domains[0].insertions) - self.assertListEqual([], strand1.domains[0].deletions) - self.assertListEqual([], strand1.domains[0].insertions) - - -class TestNickLigateAndCrossover(unittest.TestCase): - """ - Tests add_nick(), ligate(), add_*_crossover(), and remove_*_crossover() methods on Design - """ - - r""" - small_design: - 0 8 16 - ACGTACGA AACCGGTA -0 [------- -------> - <------- -------] - TGCATGCT TTGGCCAT - - AAACCCGG TTTGGGCC -1 [------- -------> - <------- -------] - TTTGGGCC AAACCCGG - - - small_nicked_design - 0 8 16 - ACGTACGA AACCGGTA -0 [------> [------> - <------] <------] - TGCATGCT TTGGCCAT - - AAACCCGG TTTGGGCC -1 [------> [------> - <------] <------] - TTTGGGCC AAACCCGG - - - design: - 0 8 16 24 32 40 48 56 64 72 80 88 96 -0 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> - <------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------] - -1 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> - <------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------] - -2 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> - <------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------] - -3 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> - <------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------] - -4 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> - <------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------] - -5 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> - <------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------] - - two_strand_design: - 0 8 16 - -0 [------- -------> - - -1 <------- -------] - """ - - def setUp(self) -> None: - # small design - strands_small_design = [ - sc.Strand([sc.Domain(0, True, 0, 16)]), - sc.Strand([sc.Domain(0, False, 0, 16)]), - sc.Strand([sc.Domain(1, True, 0, 16)]), - sc.Strand([sc.Domain(1, False, 0, 16)]), - ] - self.small_design = sc.Design(strands=strands_small_design, grid=sc.square) - self.small_design.assign_dna(strands_small_design[0], "ACGTACGA AACCGGTA") - self.small_design.assign_dna(strands_small_design[2], "AAACCCGG TTTGGGCC") - - # small nicked design - small_nicked_helices = [sc.Helix(max_offset=100) for _ in range(2)] - self.small_nicked_design = sc.Design(helices=small_nicked_helices, grid=sc.square) - # forward strands - self.small_nicked_design.draw_strand(0, 0).move(8) - self.small_nicked_design.draw_strand(0, 8).move(8) - self.small_nicked_design.draw_strand(1, 0).move(8) - self.small_nicked_design.draw_strand(1, 8).move(8) - # reverse strands - self.small_nicked_design.draw_strand(0, 8).move(-8) - self.small_nicked_design.draw_strand(0, 16).move(-8) - self.small_nicked_design.draw_strand(1, 8).move(-8) - self.small_nicked_design.draw_strand(1, 16).move(-8) - - self.small_nicked_design.assign_dna(self.small_nicked_design.strands[0], "ACGTACGA") - self.small_nicked_design.assign_dna(self.small_nicked_design.strands[1], "AACCGGTA") - self.small_nicked_design.assign_dna(self.small_nicked_design.strands[2], "AAACCCGG") - self.small_nicked_design.assign_dna(self.small_nicked_design.strands[3], "TTTGGGCC") - - # origami - self.max_offset: int = 8 * 12 - scafs = [] - staps = [] - for helix in range(6): - scaf_ss = sc.Domain(helix, helix % 2 == 0, 0, self.max_offset) - stap_ss = sc.Domain(helix, helix % 2 == 1, 0, self.max_offset) - scaf = sc.Strand([scaf_ss]) - stap = sc.Strand([stap_ss]) - scafs.append(scaf) - staps.append(stap) - self.origami: sc.Design = sc.Design(strands=scafs + staps, grid=sc.square) - - self.two_strand_design = sc.Design(helices=[sc.Helix(max_offset=100) for _ in range(2)]) - self.two_strand_design.draw_strand(0, 0).move(16) - self.two_strand_design.draw_strand(1, 16).move(-16) - - def test_add_nick__twice_on_same_domain(self) -> None: - """ - before - 0 8 16 24 - 0 [------- -------- -------> - - after - 0 8 16 24 - 0 [------> [------> [------> - """ - design = sc.Design(strands=[ - sc.Strand([sc.Domain(0, True, 0, 24)]), - ], grid=sc.square) - design.add_nick(helix=0, offset=8, forward=True) - design.add_nick(helix=0, offset=16, forward=True) - self.assertEqual(3, len(design.strands)) - self.assertIn(sc.Strand([sc.Domain(0, True, 0, 8)]), design.strands) - self.assertIn(sc.Strand([sc.Domain(0, True, 8, 16)]), design.strands) - self.assertIn(sc.Strand([sc.Domain(0, True, 16, 24)]), design.strands) - - def test_ligate__twice_on_same_domain(self) -> None: - """ - before - 0 8 16 24 - 0 [------> [------> [------> - - after - 0 8 16 24 - 0 [------- -------- -------> - """ - design = sc.Design(helices=[sc.Helix(max_offset=24)], grid=sc.square) - design.draw_strand(0, 0).move(8) - design.draw_strand(0, 8).move(8) - design.draw_strand(0, 16).move(8) - design.ligate(helix=0, offset=8, forward=True) - design.ligate(helix=0, offset=16, forward=True) - self.assertEqual(1, len(design.strands)) - self.assertIn(sc.Strand([sc.Domain(0, True, 0, 24)]), design.strands) - - def test_add_nick__small_design_no_nicks_added_yet(self) -> None: - """ - 0 8 16 - ACGTACGA AACCGGTA - 0 [------- -------> - <------- -------] - TGCATGCT TTGGCCAT - - AAACCCGG TTTGGGCC - 1 [------- -------> - <------- -------] - TTTGGGCC AAACCCGG - """ - self.assertEqual(4, len(self.small_design.strands)) - self.assertIn( - sc.Strand([sc.Domain(0, True, 0, 16, dna_sequence=remove_whitespace('ACGTACGA AACCGGTA'))]), - self.small_design.strands) - self.assertIn( - sc.Strand([sc.Domain(0, False, 0, 16, dna_sequence=remove_whitespace('TACCGGTT TCGTACGT'))]), - self.small_design.strands) - self.assertIn( - sc.Strand([sc.Domain(1, True, 0, 16, dna_sequence=remove_whitespace('AAACCCGG TTTGGGCC'))]), - self.small_design.strands) - self.assertIn( - sc.Strand([sc.Domain(1, False, 0, 16, dna_sequence=remove_whitespace('GGCCCAAA CCGGGTTT'))]), - self.small_design.strands) - # DNA - strand = strand_matching(self.small_design.strands, 0, True, 0, 16) - self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) - strand = strand_matching(self.small_design.strands, 0, False, 0, 16) - self.assertEqual(remove_whitespace('TACCGGTT TCGTACGT'), strand.dna_sequence) - strand = strand_matching(self.small_design.strands, 1, True, 0, 16) - self.assertEqual(remove_whitespace('AAACCCGG TTTGGGCC'), strand.dna_sequence) - strand = strand_matching(self.small_design.strands, 1, False, 0, 16) - self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - - def test_ligate__small_nicked_design_no_ligation_yet(self) -> None: - """ - 0 8 16 - ACGTACGA AACCGGTA - 0 [------> [------> - <------] <------] - TGCATGCT TTGGCCAT - - AAACCCGG TTTGGGCC - 1 [------> [------> - <------] <------] - TTTGGGCC AAACCCGG - """ - design = self.small_nicked_design - self.assertEqual(8, len(design.strands)) - self.assertIn(sc.Strand([sc.Domain(0, True, 0, 8, dna_sequence='ACGTACGA')]), design.strands) - self.assertIn(sc.Strand([sc.Domain(0, False, 0, 8, dna_sequence='TGCATGCT'[::-1]), ]), design.strands) - self.assertIn(sc.Strand([sc.Domain(1, True, 0, 8, dna_sequence='AAACCCGG')]), design.strands) - self.assertIn(sc.Strand([sc.Domain(1, False, 0, 8, dna_sequence='TTTGGGCC'[::-1])]), design.strands) - self.assertIn(sc.Strand([sc.Domain(0, True, 8, 16, dna_sequence='AACCGGTA')]), design.strands) - self.assertIn(sc.Strand([sc.Domain(0, False, 8, 16, dna_sequence='TTGGCCAT'[::-1])]), design.strands) - self.assertIn(sc.Strand([sc.Domain(1, True, 8, 16, dna_sequence='TTTGGGCC')]), design.strands) - self.assertIn(sc.Strand([sc.Domain(1, False, 8, 16, dna_sequence='AAACCCGG'[::-1])]), design.strands) - # DNA - strand = strand_matching(design.strands, 0, True, 0, 8) - self.assertEqual(remove_whitespace('ACGTACGA'), strand.dna_sequence) - strand = strand_matching(design.strands, 0, True, 8, 16) - self.assertEqual(remove_whitespace('AACCGGTA'), strand.dna_sequence) - - strand = strand_matching(design.strands, 0, False, 0, 8) - self.assertEqual(remove_whitespace('TCGTACGT'), strand.dna_sequence) - strand = strand_matching(design.strands, 0, False, 8, 16) - self.assertEqual(remove_whitespace('TACCGGTT'), strand.dna_sequence) - - strand = strand_matching(design.strands, 1, True, 0, 8) - self.assertEqual(remove_whitespace('AAACCCGG'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, True, 8, 16) - self.assertEqual(remove_whitespace('TTTGGGCC'), strand.dna_sequence) - - strand = strand_matching(design.strands, 1, False, 0, 8) - self.assertEqual(remove_whitespace('CCGGGTTT'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, False, 8, 16) - self.assertEqual(remove_whitespace('GGCCCAAA'), strand.dna_sequence) - - def test_add_nick__small_design_H0_forward(self) -> None: - """ - 0 8 16 - ACGTACGA AACCGGTA - 0 [------> [------> - <------- -------] - TGCATGCT TTGGCCAT - - AAACCCGG TTTGGGCC - 1 [------- -------> - <------- -------] - TTTGGGCC AAACCCGG - """ - - design = self.small_design - design.add_nick(helix=0, offset=8, forward=True) - self.assertEqual(5, len(design.strands)) - # two new Strands - self.assertIn(sc.Strand([sc.Domain(0, True, 0, 8, dna_sequence='ACGTACGA')]), design.strands) - self.assertIn(sc.Strand([sc.Domain(0, True, 8, 16, dna_sequence='AACCGGTA')]), design.strands) - # existing Strands - self.assertIn( - sc.Strand([sc.Domain(0, False, 0, 16, dna_sequence=remove_whitespace('TACCGGTT TCGTACGT'))]), - design.strands) - self.assertIn( - sc.Strand([sc.Domain(1, True, 0, 16, dna_sequence=remove_whitespace('AAACCCGG TTTGGGCC'))]), - design.strands) - self.assertIn( - sc.Strand([sc.Domain(1, False, 0, 16, dna_sequence=remove_whitespace('GGCCCAAA CCGGGTTT'))]), - design.strands) - # DNA - strand = strand_matching(design.strands, 0, True, 0, 8) - self.assertEqual(remove_whitespace('ACGTACGA'), strand.dna_sequence) - strand = strand_matching(design.strands, 0, True, 8, 16) - self.assertEqual(remove_whitespace('AACCGGTA'), strand.dna_sequence) - strand = strand_matching(design.strands, 0, False, 0, 16) - self.assertEqual(remove_whitespace('TACCGGTT TCGTACGT'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, True, 0, 16) - self.assertEqual(remove_whitespace('AAACCCGG TTTGGGCC'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, False, 0, 16) - self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - - def test_ligate__small_nicked_design_ligate_all(self) -> None: - """ - 0 8 16 - ACGTACGA AACCGGTA - 0 [------- -------> - <------- -------] - TGCATGCT TTGGCCAT - - AAACCCGG TTTGGGCC - 1 [------- -------> - <------- -------] - TTTGGGCC AAACCCGG - """ - design = self.small_nicked_design - design.ligate(0, 8, True) - design.ligate(0, 8, False) - design.ligate(1, 8, True) - design.ligate(1, 8, False) - self.assertEqual(4, len(design.strands)) - self.assertIn(sc.Strand([sc.Domain(0, True, 0, 16, dna_sequence='ACGTACGAAACCGGTA')]), design.strands) - self.assertIn( - sc.Strand([sc.Domain(0, False, 0, 16, dna_sequence='TGCATGCTTTGGCCAT'[:: -1])]), - design.strands) - self.assertIn(sc.Strand([sc.Domain(1, True, 0, 16, dna_sequence='AAACCCGGTTTGGGCC')]), design.strands) - self.assertIn( - sc.Strand([sc.Domain(1, False, 0, 16, dna_sequence='TTTGGGCCAAACCCGG'[:: -1])]), - design.strands) - # DNA - strand = strand_matching(design.strands, 0, True, 0, 16) - self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) - strand = strand_matching(design.strands, 0, False, 0, 16) - self.assertEqual(remove_whitespace('TACCGGTT TCGTACGT'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, True, 0, 16) - self.assertEqual(remove_whitespace('AAACCCGG TTTGGGCC'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, False, 0, 16) - self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - - def test_add_nick__small_design_H0_reverse(self) -> None: - """ - 0 8 16 - ACGTACGA AACCGGTA - 0 [------- -------> - <------] <------] - TGCATGCT TTGGCCAT - - AAACCCGG TTTGGGCC - 1 [------- -------> - <------- -------] - TTTGGGCC AAACCCGG - """ - design = self.small_design - design.add_nick(helix=0, offset=8, forward=False) - self.assertEqual(5, len(design.strands)) - # two new Strands - self.assertIn( - sc.Strand([sc.Domain(0, False, 0, 8, dna_sequence=remove_whitespace('TCGTACGT'))]), - design.strands) - self.assertIn( - sc.Strand([sc.Domain(0, False, 8, 16, dna_sequence=remove_whitespace('TACCGGTT'))]), - design.strands) - # existing Strands - self.assertIn( - sc.Strand([sc.Domain(0, True, 0, 16, dna_sequence=remove_whitespace('ACGTACGA AACCGGTA'))]), - design.strands) - self.assertIn( - sc.Strand([sc.Domain(1, True, 0, 16, dna_sequence=remove_whitespace('AAACCCGG TTTGGGCC'))]), - design.strands) - self.assertIn( - sc.Strand([sc.Domain(1, False, 0, 16, dna_sequence=remove_whitespace('GGCCCAAA CCGGGTTT'))]), - design.strands) - # DNA - strand = strand_matching(design.strands, 0, True, 0, 16) - self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) - strand = strand_matching(design.strands, 0, False, 8, 16) - self.assertEqual(remove_whitespace('TACCGGTT'), strand.dna_sequence) - strand = strand_matching(design.strands, 0, False, 0, 8) - self.assertEqual(remove_whitespace('TCGTACGT'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, True, 0, 16) - self.assertEqual(remove_whitespace('AAACCCGG TTTGGGCC'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, False, 0, 16) - self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - - def test_add_nick__small_design_H1_forward(self) -> None: - """ - 0 8 16 - ACGTACGA AACCGGTA - 0 [------- -------> - <------- -------] - TGCATGCT TTGGCCAT - - AAACCCGG TTTGGGCC - 1 [------> [------> - <------- -------] - TTTGGGCC AAACCCGG - """ - design = self.small_design - design.add_nick(helix=1, offset=8, forward=True) - self.assertEqual(5, len(design.strands)) - # two new Strands - self.assertIn( - sc.Strand([sc.Domain(1, True, 0, 8, dna_sequence=remove_whitespace('AAACCCGG'))]), - design.strands) - self.assertIn( - sc.Strand([sc.Domain(1, True, 8, 16, dna_sequence=remove_whitespace('TTTGGGCC'))]), - design.strands) - # existing Strands - self.assertIn( - sc.Strand([sc.Domain(1, False, 0, 16, dna_sequence=remove_whitespace('GGCCCAAA CCGGGTTT'))]), - design.strands) - self.assertIn( - sc.Strand([sc.Domain(0, True, 0, 16, dna_sequence=remove_whitespace('ACGTACGA AACCGGTA'))]), - design.strands) - self.assertIn( - sc.Strand([sc.Domain(0, False, 0, 16, dna_sequence=remove_whitespace('TACCGGTT TCGTACGT'))]), - design.strands) - # DNA - strand = strand_matching(design.strands, 0, True, 0, 16) - self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) - strand = strand_matching(design.strands, 0, False, 0, 16) - self.assertEqual(remove_whitespace('TACCGGTT TCGTACGT'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, True, 0, 8) - self.assertEqual(remove_whitespace('AAACCCGG'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, True, 8, 16) - self.assertEqual(remove_whitespace('TTTGGGCC'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, False, 0, 16) - self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - - def test_add_nick__small_design_H1_reverse(self) -> None: - """ - 0 8 16 - ACGTACGA AACCGGTA - 0 [------- -------> - <------- -------] - TGCATGCT TTGGCCAT - - AAACCCGG TTTGGGCC - 1 [------- -------> - <------] <------] - TTTGGGCC AAACCCGG - """ - design = self.small_design - design.add_nick(helix=1, offset=8, forward=False) - self.assertEqual(5, len(design.strands)) - # two new Strands - self.assertIn( - sc.Strand([sc.Domain(1, False, 0, 8, dna_sequence=remove_whitespace('CCGGGTTT'))]), - design.strands) - self.assertIn( - sc.Strand([sc.Domain(1, False, 8, 16, dna_sequence=remove_whitespace('GGCCCAAA'))]), - design.strands) - # existing Strands - self.assertIn( - sc.Strand([sc.Domain(1, True, 0, 16, dna_sequence=remove_whitespace('AAACCCGG TTTGGGCC'))]), - design.strands) - self.assertIn( - sc.Strand([sc.Domain(0, True, 0, 16, dna_sequence=remove_whitespace('ACGTACGA AACCGGTA'))]), - design.strands) - self.assertIn( - sc.Strand([sc.Domain(0, False, 0, 16, dna_sequence=remove_whitespace('TACCGGTT TCGTACGT'))]), - design.strands) - # DNA - strand = strand_matching(design.strands, 0, True, 0, 16) - self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) - strand = strand_matching(design.strands, 0, False, 0, 16) - self.assertEqual(remove_whitespace('TACCGGTT TCGTACGT'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, True, 0, 16) - self.assertEqual(remove_whitespace('AAACCCGG TTTGGGCC'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, False, 0, 8) - self.assertEqual(remove_whitespace('CCGGGTTT'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, False, 8, 16) - self.assertEqual(remove_whitespace('GGCCCAAA'), strand.dna_sequence) - - def test_add_full_crossover__small_design_H0_forward(self) -> None: - """ - 0 8 16 - ACGTACGA AACCGGTA - 0 [------+ +------> - <------- -------] - TGCATGCT TTGGCCAT - | | - AAACCCGG TTTGGGCC - 1 [------- -------> - <------+ +------] - TTTGGGCC AAACCCGG - """ - design = self.small_design - design.add_full_crossover(helix=0, helix2=1, offset=8, forward=True) - self.assertEqual(4, len(design.strands)) - # two new Strands - self.assertIn(sc.Strand([ - sc.Domain(0, True, 0, 8, dna_sequence=remove_whitespace('ACGTACGA')), - sc.Domain(1, False, 0, 8, dna_sequence=remove_whitespace('TTTGGGCC'[::-1])), - ]), design.strands) - self.assertIn(sc.Strand([ - sc.Domain(1, False, 8, 16, dna_sequence=remove_whitespace('AAACCCGG'[::-1])), - sc.Domain(0, True, 8, 16, dna_sequence=remove_whitespace('AACCGGTA')), - ]), design.strands) - # existing Strands - self.assertIn(sc.Strand([ - sc.Domain(0, False, 0, 16, dna_sequence=remove_whitespace('TGCATGCT TTGGCCAT'[::-1])) - ]), design.strands) - self.assertIn(sc.Strand([ - sc.Domain(1, True, 0, 16, dna_sequence=remove_whitespace('AAACCCGG TTTGGGCC')) - ]), design.strands) - # DNA - strand = strand_matching(design.strands, 0, False, 0, 16) - self.assertEqual(remove_whitespace('TACCGGTT TCGTACGT'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, True, 0, 16) - self.assertEqual(remove_whitespace('AAACCCGG TTTGGGCC'), strand.dna_sequence) - strand = strand_matching(design.strands, 0, True, 0, 8) - self.assertEqual(remove_whitespace('ACGTACGA CCGGGTTT'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, False, 8, 16) - self.assertEqual(remove_whitespace('GGCCCAAA AACCGGTA'), strand.dna_sequence) - - def test_add_full_crossover__small_design_H0_reverse(self) -> None: - """ - 0 8 16 - ACGTACGA AACCGGTA - 0 [------- -------> - <------+ +------] - TGCATGCT TTGGCCAT - | | - AAACCCGG TTTGGGCC - 1 [------+ +------> - <------- -------] - TTTGGGCC AAACCCGG - """ - design = self.small_design - design.add_full_crossover(helix=0, helix2=1, offset=8, forward=False) - self.assertEqual(4, len(design.strands)) - # two new Strands - self.assertIn(sc.Strand([ - sc.Domain(1, True, 0, 8, dna_sequence='AAACCCGG'), - sc.Domain(0, False, 0, 8, dna_sequence='TGCATGCT'[::-1]), - ]), design.strands) - self.assertIn(sc.Strand([ - sc.Domain(0, False, 8, 16, dna_sequence='TTGGCCAT'[::-1]), - sc.Domain(1, True, 8, 16, dna_sequence='TTTGGGCC'), - ]), design.strands) - # existing Strands - self.assertIn(sc.Strand([ - sc.Domain(0, True, 0, 16, dna_sequence='ACGTACGAAACCGGTA') - ]), design.strands) - self.assertIn(sc.Strand([ - sc.Domain(1, False, 0, 16, dna_sequence='TTTGGGCCAAACCCGG'[:: -1]) - ]), design.strands) - # DNA - strand = strand_matching(design.strands, 0, True, 0, 16) - self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) - strand = strand_matching(design.strands, 0, False, 8, 16) - self.assertEqual(remove_whitespace('TACCGGTT TTTGGGCC'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, True, 0, 8) - self.assertEqual(remove_whitespace('AAACCCGG TCGTACGT'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, False, 0, 16) - self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - - def test_add_full_crossover__horizontal_crossovers_already_there(self) -> None: - """ - 0 8 16 - 0 [------+^+------> - 1 <------+^+------] - """ - design = sc.Design(helices=[sc.Helix(16) for _ in range(2)]) - design.draw_strand(0, 0).move(8).move(8) - design.draw_strand(1, 16).move(-8).move(-8) - - self.assertEqual(2, len(design.strands)) - - with self.assertRaises(sc.IllegalDesignError) as ctx: - design.add_full_crossover(helix=0, helix2=1, offset=8, forward=True) - msg = str(ctx.exception) - self.assertIn('already a crossover', msg) - - def test_add_full_crossover__top_horizontal_crossover_already_there(self) -> None: - """ - 0 8 16 - 0 [------+^+------> - 1 <------] <------] - """ - design = sc.Design(helices=[sc.Helix(16) for _ in range(2)]) - design.draw_strand(0, 0).move(8).move(8) - design.draw_strand(1, 16).move(-8) - design.draw_strand(1, 8).move(-8) - - self.assertEqual(3, len(design.strands)) - - with self.assertRaises(sc.IllegalDesignError) as ctx: - design.add_full_crossover(helix=0, helix2=1, offset=8, forward=True) - msg = str(ctx.exception) - self.assertIn('already a crossover', msg) - - def test_add_full_crossover__bottom_horizontal_crossover_already_there(self) -> None: - """ - 0 8 16 - 0 [------> [------> - 1 <------+^+------] - """ - design = sc.Design(helices=[sc.Helix(16) for _ in range(2)]) - design.draw_strand(0, 0).move(8) - design.draw_strand(0, 8).move(8) - design.draw_strand(1, 16).move(-8).move(-8) - - self.assertEqual(3, len(design.strands)) - - with self.assertRaises(sc.IllegalDesignError) as ctx: - design.add_full_crossover(helix=0, helix2=1, offset=8, forward=True) - msg = str(ctx.exception) - self.assertIn('already a crossover', msg) - - def test_add_half_crossover__horizontal_crossovers_already_there(self) -> None: - """ - 0 8 16 - 0 [------+^+------> - 1 <------+^+------] - """ - design = sc.Design(helices=[sc.Helix(16) for _ in range(2)]) - design.draw_strand(0, 0).move(8).move(8) - design.draw_strand(1, 16).move(-8).move(-8) - - self.assertEqual(2, len(design.strands)) - - with self.assertRaises(sc.IllegalDesignError) as ctx: - design.add_half_crossover(helix=0, helix2=1, offset=8, forward=True) - msg = str(ctx.exception) - self.assertIn('is expected to be on the', msg) # both 3' and 5' are problems, so just make sure - self.assertIn('end of the strand', msg) # one of them is mentioned here - - def test_add_half_crossover__top_horizontal_crossover_already_there(self) -> None: - """ - 0 8 16 - 0 [------+^+------> - 1 <------] <------] - """ - design = sc.Design(helices=[sc.Helix(16) for _ in range(2)]) - design.draw_strand(0, 0).move(8).move(8) - design.draw_strand(1, 16).move(-8) - design.draw_strand(1, 8).move(-8) - - self.assertEqual(3, len(design.strands)) - - with self.assertRaises(sc.IllegalDesignError) as ctx: - design.add_half_crossover(helix=0, helix2=1, offset=8, forward=True) - msg = str(ctx.exception) - self.assertIn("is expected to be on the 5' end of the strand", msg) - - def test_add_half_crossover__bottom_horizontal_crossover_already_there(self) -> None: - """ - 0 8 16 - 0 [------> [------> - 1 <------+^+------] - """ - design = sc.Design(helices=[sc.Helix(16) for _ in range(2)]) - design.draw_strand(0, 0).move(8) - design.draw_strand(0, 8).move(8) - design.draw_strand(1, 16).move(-8).move(-8) - - self.assertEqual(3, len(design.strands)) - - with self.assertRaises(sc.IllegalDesignError) as ctx: - design.add_half_crossover(helix=0, helix2=1, offset=8, forward=True) - msg = str(ctx.exception) - self.assertIn("is expected to be on the 3' end of the strand", msg) - - def test_add_half_crossover__small_design_H0_reverse_8(self) -> None: - """ - 0 8 16 - ACGTACGA AACCGGTA - 0 [------- -------> - <------] +------] - TGCATGCT TTGGCCAT - | - AAACCCGG TTTGGGCC - 1 [------> +------> - <------- -------] - TTTGGGCC AAACCCGG - """ - design = self.small_design - design.add_nick(helix=0, offset=8, forward=False) - design.add_nick(helix=1, offset=8, forward=True) - design.add_half_crossover(helix=0, helix2=1, offset=8, forward=False) - self.assertEqual(5, len(design.strands)) - # three new Strands - self.assertIn(sc.Strand([ - sc.Domain(1, True, 0, 8, dna_sequence=remove_whitespace('AAACCCGG')), - ]), design.strands) - self.assertIn(sc.Strand([ - sc.Domain(0, False, 0, 8, dna_sequence=remove_whitespace('TCGTACGT')), - ]), design.strands) - self.assertIn(sc.Strand([ - sc.Domain(0, False, 8, 16, dna_sequence=remove_whitespace('TACCGGTT')), - sc.Domain(1, True, 8, 16, dna_sequence=remove_whitespace('TTTGGGCC')), - ]), design.strands) - # existing Strands - self.assertIn(sc.Strand([ - sc.Domain(0, True, 0, 16, dna_sequence=remove_whitespace('ACGTACGA AACCGGTA')), - ]), design.strands) - self.assertIn(sc.Strand([ - sc.Domain(1, False, 0, 16, dna_sequence=remove_whitespace('GGCCCAAA CCGGGTTT')), - ]), design.strands) - # DNA - strand = strand_matching(design.strands, 0, True, 0, 16) - self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) - strand = strand_matching(design.strands, 0, False, 8, 16) - self.assertEqual(remove_whitespace('TACCGGTT TTTGGGCC'), strand.dna_sequence) - strand = strand_matching(design.strands, 0, False, 0, 8) - self.assertEqual(remove_whitespace('TCGTACGT'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, True, 0, 8) - self.assertEqual(remove_whitespace('AAACCCGG'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, False, 0, 16) - self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - - def test_add_half_crossover__both_scaffolds_preserved(self) -> None: - """ - 0 8 16 - - 0 [------- -------> scaffold - - - 1 <------- -------] scaffold - - """ - design: sc.Design = self.two_strand_design - design.strands[0].set_scaffold(True) - design.strands[1].set_scaffold(True) - design.add_half_crossover(helix=0, helix2=1, offset=15, forward=True) - self.assertEqual(1, len(design.strands)) - self.assertTrue(design.strands[0].is_scaffold) - self.assertIsNotNone(design.scaffold) - - def test_add_half_crossover__first_scaffold_preserved(self) -> None: - """ - 0 8 16 - - 0 [------- -------> scaffold - - - 1 <------- -------] - - """ - design: sc.Design = self.two_strand_design - design.strands[0].set_scaffold(True) - design.add_half_crossover(helix=0, helix2=1, offset=15, forward=True) - self.assertEqual(1, len(design.strands)) - self.assertTrue(design.strands[0].is_scaffold) - self.assertIsNotNone(design.scaffold) - - def test_add_half_crossover__second_scaffold_preserved(self) -> None: - """ - 0 8 16 - - 0 [------- -------> - - - 1 <------- -------] scaffold - - """ - design: sc.Design = self.two_strand_design - design.strands[1].set_scaffold(True) - design.add_half_crossover(helix=0, helix2=1, offset=15, forward=True) - self.assertEqual(1, len(design.strands)) - self.assertTrue(design.strands[0].is_scaffold) - self.assertIsNotNone(design.scaffold) - - def test_add_half_crossover__neither_scaffold_preserved(self) -> None: - """ - 0 8 16 - - 0 [------- -------> - - - 1 <------- -------] - - """ - design: sc.Design = self.two_strand_design - design.add_half_crossover(helix=0, helix2=1, offset=15, forward=True) - self.assertEqual(1, len(design.strands)) - self.assertFalse(design.strands[0].is_scaffold) - self.assertIsNone(design.scaffold) - - def test_add_half_crossover__small_design_H0_reverse_0(self) -> None: - """ - 0 8 16 - ACGTACGA AACCGGTA - 0 [------- -------> - +------- -------] - TGCATGCT TTGGCCAT - | - AAACCCGG TTTGGGCC - 1 -------- -------> - <------- -------] - TTTGGGCC AAACCCGG - """ - design = self.small_design - design.add_half_crossover(helix=0, helix2=1, offset=0, forward=False) - self.assertEqual(3, len(design.strands)) - # one new Strand - self.assertIn(sc.Strand([ - sc.Domain(0, False, 0, 16, dna_sequence=remove_whitespace('TGCATGCT TTGGCCAT'[::-1])), - sc.Domain(1, True, 0, 16, dna_sequence=remove_whitespace('AAACCCGG TTTGGGCC')), - ]), design.strands) - # existing Strands - self.assertIn(sc.Strand([ - sc.Domain(0, True, 0, 16, dna_sequence=remove_whitespace('ACGTACGA AACCGGTA')), - ]), design.strands) - self.assertIn(sc.Strand([ - sc.Domain(1, False, 0, 16, dna_sequence=remove_whitespace('TTTGGGCC AAACCCGG'[::-1])), - ]), design.strands) - # DNA - strand = strand_matching(design.strands, 0, True, 0, 16) - self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) - strand = strand_matching(design.strands, 0, False, 0, 16) - self.assertEqual(remove_whitespace('TACCGGTT TCGTACGT AAACCCGG TTTGGGCC'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, False, 0, 16) - self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - - def test_add_half_crossover__small_design_H0_reverse_15(self) -> None: - """ - 0 8 16 - ACGTACGA AACCGGTA - 0 [------- -------> - <------- -------+ - TGCATGCT TTGGCCAT - | - AAACCCGG TTTGGGCC - 1 [------- -------+ - <------- -------] - TTTGGGCC AAACCCGG - """ - design = self.small_design - design.add_half_crossover(helix=0, helix2=1, offset=15, forward=False) - self.assertEqual(3, len(design.strands)) - # one new Strand - self.assertIn(sc.Strand([ - sc.Domain(1, True, 0, 16, dna_sequence=remove_whitespace('AAACCCGG TTTGGGCC')), - sc.Domain(0, False, 0, 16, dna_sequence=remove_whitespace('TGCATGCT TTGGCCAT'[::-1])), - ]), design.strands) - # existing Strands - self.assertIn( - sc.Strand([sc.Domain(0, True, 0, 16, dna_sequence=remove_whitespace('ACGTACGA AACCGGTA'))]), - design.strands) - self.assertIn( - sc.Strand([sc.Domain(1, False, 0, 16, dna_sequence=remove_whitespace('GGCCCAAA CCGGGTTT'))]), - design.strands) - # DNA - strand = strand_matching(design.strands, 0, True, 0, 16) - self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, True, 0, 16) - self.assertEqual(remove_whitespace('AAACCCGG TTTGGGCC TACCGGTT TCGTACGT'), strand.dna_sequence) - strand = strand_matching(design.strands, 1, False, 0, 16) - self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) - - def test_add_half_crossover__small_design_illegal(self) -> None: - """ - 0 8 16 - 0 [------- -------> - <------- -------- ? - | - 1 [------- -------- ? - <------- -------] - """ - with self.assertRaises(sc.IllegalDesignError): - self.small_design.add_half_crossover(helix=0, helix2=1, offset=16, forward=False) - - def test_add_full_crossover__small_design_illegal(self) -> None: - """ - 0 8 16 - 0 [------- -------> - <------- -------+ ? - | | - 1 [------- -------+ ? - <------- -------] - """ - with self.assertRaises(sc.IllegalDesignError): - self.small_design.add_full_crossover(helix=0, helix2=1, offset=16, forward=False) - - def test_add_full_crossover__small_design_illegal_only_one_helix_has_domain(self) -> None: - """ - 0 8 16 - 0 [------- -------> - <------+ +------] - | | - 1 [---> ? ? - <---] - """ - design = sc.Design(strands=[ - sc.Strand([sc.Domain(0, True, 0, 16)]), - sc.Strand([sc.Domain(0, False, 0, 16)]), - sc.Strand([sc.Domain(1, True, 0, 5)]), - sc.Strand([sc.Domain(1, False, 0, 5)]), - ], grid=sc.square) - with self.assertRaises(sc.IllegalDesignError): - design.add_full_crossover(helix=0, helix2=1, offset=10, forward=False) - - r""" - 0 8 16 24 32 40 48 56 64 72 80 88 96 -0 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> - <------- -------- -------- -------- -------] <------- -------- -------- -------] <------- -------- -------] - -1 [------- -------- -------> [------- -------- -------- -------> [------- -------- -------- -------- -------> - <------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------] - -2 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> - <------- -------- -------- -------- -------] <------- -------- -------- -------] <------- -------- -------] - -3 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> - <------- -------- -------> [------- -------- -------- -------> [------- -------- -------- -------- -------] - -4 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> - <------- -------- -------- -------- -------] <------- -------- -------- -------] <------- -------- -------] - -5 [------- -------- -------> [------- -------- -------- -------> [------- -------- -------- -------- -------> - <------- -------- -------- -------- -------- -------] <------- -------- -------- -------- -------- -------] - """ - - @staticmethod - def add_nicks(design: sc.Design): - design.add_nick(helix=5, offset=48, forward=False) - design.add_nick(helix=0, offset=40, forward=False) - design.add_nick(helix=0, offset=72, forward=False) - design.add_nick(helix=2, offset=40, forward=False) - design.add_nick(helix=2, offset=72, forward=False) - design.add_nick(helix=4, offset=40, forward=False) - design.add_nick(helix=4, offset=72, forward=False) - design.add_nick(helix=1, offset=24, forward=True) - design.add_nick(helix=1, offset=56, forward=True) - design.add_nick(helix=3, offset=24, forward=True) - design.add_nick(helix=3, offset=56, forward=True) - design.add_nick(helix=5, offset=24, forward=True) - design.add_nick(helix=5, offset=56, forward=True) - - def test_add_nick__6_helix_rectangle(self) -> None: - self.add_nicks(self.origami) - self.assertEqual(25, len(self.origami.strands)) - for helix in range(0, len(self.origami.helices), 2): - # even helix - self.assertIn(sc.Strand([sc.Domain(helix, True, 0, 96)]), self.origami.strands) - self.assertIn(sc.Strand([sc.Domain(helix, False, 0, 40)]), self.origami.strands) - self.assertIn(sc.Strand([sc.Domain(helix, False, 40, 72)]), self.origami.strands) - self.assertIn(sc.Strand([sc.Domain(helix, False, 72, 96)]), self.origami.strands) - # odd helix - if helix + 1 < len(self.origami.helices) - 1: - self.assertIn(sc.Strand([sc.Domain(helix + 1, False, 0, 96)]), self.origami.strands) - else: - # nick in scaffold on bottom helix - self.assertIn(sc.Strand([sc.Domain(helix + 1, False, 0, 48)]), self.origami.strands) - self.assertIn(sc.Strand([sc.Domain(helix + 1, False, 48, 96)]), self.origami.strands) - self.assertIn(sc.Strand([sc.Domain(helix + 1, True, 0, 24)]), self.origami.strands) - self.assertIn(sc.Strand([sc.Domain(helix + 1, True, 24, 56)]), self.origami.strands) - self.assertIn(sc.Strand([sc.Domain(helix + 1, True, 56, 96)]), self.origami.strands) - - # TODO: re-write this test after support for circular Strands is added and test making crossovers first - r""" - Crossovers needed to be added after nicks so that there will be no circular strands - 0 8 16 24 32 40 48 56 64 72 80 88 96 -0 +------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------+ - /<------- -------+ +------- -------- -------] <------- -------- -------- -------] <------+ +------- -------]\ - ( | | | | ) -1 \[------- -------+ +------> [------+ +------- -------- -------> [------+ +------- -------+ +------- ------->/ - +------- -------- -------- -------- -------- -------+ +------- -------- -------- -------- -------- -------+ - | | | | | | -2 +------- -------- -------- -------- -------- -------+ +------- -------- -------- -------- -------- -------+ - /<------- -------+ +------- -------+ +------] <------- -------- -------+ +------] <------+ +------- -------]\ - ( | | | | ) -3 \[------- -------+ +------> [------+ +------- -------- -------> [------+ +------- -------+ +------- ------->/ - +------- -------- -------- -------- -------- -------+ +------- -------- -------- -------- -------- -------+ - | | | | | | -4 +------- -------- -------- -------- -------- -------+ +------- -------- -------- -------- -------- -------+ - /<------- -------+ +------- -------+ +------] <------- -------- -------+ +------] <------+ +------- -------]\ - ( | | | | ) -5 \[------- -------+ +------> [------- -------- -------- -------> [------- -------- -------+ +------- ------->/ - +------- -------- -------- -------- -------- -------] <------- -------- -------- -------- -------- -------+ - - """ - - @staticmethod - def add_crossovers_after_nicks(design: sc.Design): - # scaffold seam crossovers - design.add_full_crossover(helix=1, helix2=2, offset=48, forward=False) - design.add_full_crossover(helix=3, helix2=4, offset=48, forward=False) - - # staple crossovers - design.add_full_crossover(helix=0, helix2=1, offset=16, forward=False) - design.add_full_crossover(helix=0, helix2=1, offset=80, forward=False) - design.add_full_crossover(helix=1, helix2=2, offset=32, forward=True) - design.add_full_crossover(helix=1, helix2=2, offset=64, forward=True) - design.add_full_crossover(helix=2, helix2=3, offset=16, forward=False) - design.add_full_crossover(helix=2, helix2=3, offset=80, forward=False) - design.add_full_crossover(helix=3, helix2=4, offset=32, forward=True) - design.add_full_crossover(helix=3, helix2=4, offset=64, forward=True) - design.add_full_crossover(helix=4, helix2=5, offset=16, forward=False) - design.add_full_crossover(helix=4, helix2=5, offset=80, forward=False) - - # The left and right edge crossovers need to be added last to ensure the Strands remain - # non-circular during all intermediate stages. - - # scaffold left crossovers - design.add_half_crossover(helix=0, helix2=1, offset=0, forward=True) - design.add_half_crossover(helix=2, helix2=3, offset=0, forward=True) - design.add_half_crossover(helix=4, helix2=5, offset=0, forward=True) - - # scaffold right crossovers - design.add_half_crossover(helix=0, helix2=1, offset=95, forward=True) - design.add_half_crossover(helix=2, helix2=3, offset=95, forward=True) - design.add_half_crossover(helix=4, helix2=5, offset=95, forward=True) - - def test_add_nick_then_add_crossovers__6_helix_rectangle(self) -> None: - self.add_nicks(self.origami) - self.add_crossovers_after_nicks(self.origami) - - self.assertEqual(19, len(self.origami.strands)) - - # staples left edge - # {"helix": 1, "forward": true, "start": 0, "end": 16}, - # {"helix": 0, "forward": false, "start": 0, "end": 16} - stap = sc.Strand([ - sc.Domain(1, True, 0, 16), - sc.Domain(0, False, 0, 16), - ]) - self.assertIn(stap, self.origami.strands) - - # {"helix": 3, "forward": true, "start": 0, "end": 16}, - # {"helix": 2, "forward": false, "start": 0, "end": 16} - stap = sc.Strand([ - sc.Domain(3, True, 0, 16), - sc.Domain(2, False, 0, 16), - ]) - self.assertIn(stap, self.origami.strands) - - # {"helix": 5, "forward": true, "start": 0, "end": 16}, - # {"helix": 4, "forward": false, "start": 0, "end": 16} - stap = sc.Strand([ - sc.Domain(5, True, 0, 16), - sc.Domain(4, False, 0, 16), - ]) - self.assertIn(stap, self.origami.strands) - - # staples right edge - # {"helix": 0, "forward": false, "start": 80, "end": 96}, - # {"helix": 1, "forward": true, "start": 80, "end": 96} - stap = sc.Strand([ - sc.Domain(0, False, 80, 96), - sc.Domain(1, True, 80, 96), - ]) - self.assertIn(stap, self.origami.strands) - - # {"helix": 2, "forward": false, "start": 80, "end": 96}, - # {"helix": 3, "forward": true, "start": 80, "end": 96} - stap = sc.Strand([ - sc.Domain(2, False, 80, 96), - sc.Domain(3, True, 80, 96), - ]) - self.assertIn(stap, self.origami.strands) - - # {"helix": 4, "forward": false, "start": 80, "end": 96}, - # {"helix": 5, "forward": true, "start": 80, "end": 96} - stap = sc.Strand([ - sc.Domain(4, False, 80, 96), - sc.Domain(5, True, 80, 96), - ]) - self.assertIn(stap, self.origami.strands) - - # staples remainder - # {"helix": 0, "forward": false, "start": 40, "end": 72} - stap = sc.Strand([sc.Domain(0, False, 40, 72)]) - self.assertIn(stap, self.origami.strands) - - # {"helix": 2, "forward": false, "start": 32, "end": 40}, - # {"helix": 1, "forward": true, "start": 32, "end": 56} - stap = sc.Strand([ - sc.Domain(2, False, 32, 40), - sc.Domain(1, True, 32, 56), - ]) - self.assertIn(stap, self.origami.strands) - - # {"helix": 1, "forward": true, "start": 56, "end": 64}, - # {"helix": 2, "forward": false, "start": 40, "end": 64} - stap = sc.Strand([ - sc.Domain(1, True, 56, 64), - sc.Domain(2, False, 40, 64), - ]) - self.assertIn(stap, self.origami.strands) - - # {"helix": 4, "forward": false, "start": 32, "end": 40}, - # {"helix": 3, "forward": true, "start": 32, "end": 56} - stap = sc.Strand([ - sc.Domain(4, False, 32, 40), - sc.Domain(3, True, 32, 56), - ]) - self.assertIn(stap, self.origami.strands) - - # {"helix": 3, "forward": true, "start": 56, "end": 64}, - # {"helix": 4, "forward": false, "start": 40, "end": 64} - stap = sc.Strand([ - sc.Domain(3, True, 56, 64), - sc.Domain(4, False, 40, 64), - ]) - self.assertIn(stap, self.origami.strands) - - # {"helix": 5, "forward": true, "start": 24, "end": 56} - stap = sc.Strand([sc.Domain(5, True, 24, 56)]) - self.assertIn(stap, self.origami.strands) - - # {"helix": 0, "forward": false, "start": 16, "end": 40}, - # {"helix": 1, "forward": true, "start": 16, "end": 24} - stap = sc.Strand([ - sc.Domain(0, False, 16, 40), - sc.Domain(1, True, 16, 24), - ]) - self.assertIn(stap, self.origami.strands) - - # {"helix": 1, "forward": true, "start": 24, "end": 32}, - # {"helix": 2, "forward": false, "start": 16, "end": 32}, - # {"helix": 3, "forward": true, "start": 16, "end": 24} - stap = sc.Strand([ - sc.Domain(1, True, 24, 32), - sc.Domain(2, False, 16, 32), - sc.Domain(3, True, 16, 24), - ]) - self.assertIn(stap, self.origami.strands) - - # {"helix": 3, "forward": true, "start": 24, "end": 32}, - # {"helix": 4, "forward": false, "start": 16, "end": 32}, - # {"helix": 5, "forward": true, "start": 16, "end": 24} - stap = sc.Strand([ - sc.Domain(3, True, 24, 32), - sc.Domain(4, False, 16, 32), - sc.Domain(5, True, 16, 24), - ]) - self.assertIn(stap, self.origami.strands) - - # {"helix": 5, "forward": true, "start": 56, "end": 80}, - # {"helix": 4, "forward": false, "start": 72, "end": 80} - stap = sc.Strand([ - sc.Domain(5, True, 56, 80), - sc.Domain(4, False, 72, 80), - ]) - self.assertIn(stap, self.origami.strands) - - # {"helix": 2, "forward": false, "start": 64, "end": 72}, - # {"helix": 1, "forward": true, "start": 64, "end": 80}, - # {"helix": 0, "forward": false, "start": 72, "end": 80} - stap = sc.Strand([ - sc.Domain(2, False, 64, 72), - sc.Domain(1, True, 64, 80), - sc.Domain(0, False, 72, 80), - ]) - self.assertIn(stap, self.origami.strands) - - # {"helix": 4, "forward": false, "start": 64, "end": 72}, - # {"helix": 3, "forward": true, "start": 64, "end": 80}, - # {"helix": 2, "forward": false, "start": 72, "end": 80} - stap = sc.Strand([ - sc.Domain(4, False, 64, 72), - sc.Domain(3, True, 64, 80), - sc.Domain(2, False, 72, 80), - ]) - self.assertIn(stap, self.origami.strands) - - # scaffold - # {"helix": 5, "forward": false, "start": 0, "end": 48}, - # {"helix": 4, "forward": true, "start": 0, "end": 48}, - # {"helix": 3, "forward": false, "start": 0, "end": 48}, - # {"helix": 2, "forward": true, "start": 0, "end": 48}, - # {"helix": 1, "forward": false, "start": 0, "end": 48}, - # {"helix": 0, "forward": true, "start": 0, "end": 96}, - # {"helix": 1, "forward": false, "start": 48, "end": 96}, - # {"helix": 2, "forward": true, "start": 48, "end": 96}, - # {"helix": 3, "forward": false, "start": 48, "end": 96}, - # {"helix": 4, "forward": true, "start": 48, "end": 96}, - # {"helix": 5, "forward": false, "start": 48, "end": 96} - scaf = sc.Strand([ - sc.Domain(5, False, 0, 48), - sc.Domain(4, True, 0, 48), - sc.Domain(3, False, 0, 48), - sc.Domain(2, True, 0, 48), - sc.Domain(1, False, 0, 48), - sc.Domain(0, True, 0, 96), - sc.Domain(1, False, 48, 96), - sc.Domain(2, True, 48, 96), - sc.Domain(3, False, 48, 96), - sc.Domain(4, True, 48, 96), - sc.Domain(5, False, 48, 96), - ]) - self.assertIn(scaf, self.origami.strands) - - def test_ligate_on_middle_domain_should_error_3p_case(self) -> None: - """ - Error to ligate here - | - v - [-----+[-----> - | - <-----+ - """ - 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) - - with self.assertRaises(sc.IllegalDesignError): - design.ligate(0, 10, True) - - def test_ligate_on_middle_domain_should_error_5p_case(self) -> None: - """ - Error to ligate here - | - v - [----->+-----> - | - +-----] - """ - design: sc.Design = sc.Design(helices=[sc.Helix(max_offset=100), sc.Helix(max_offset=100)]) - design.draw_strand(0, 0).to(10) - design.draw_strand(1, 20).to(10).cross(0).to(20) - - with self.assertRaises(sc.IllegalDesignError): - design.ligate(0, 10, True) - - def test_ligate_on_extension_side_should_error(self) -> None: - """ - ↗ - / - / - [-------[-----> - ^ - | - error to ligate here - """ - design: sc.Design = sc.Design(helices=[sc.Helix(max_offset=100)]) - design.draw_strand(0, 0).to(10).extension_3p(5) - design.draw_strand(0, 10).to(20) - with self.assertRaises(sc.IllegalDesignError): - design.ligate(0, 10, True) - - def test_ligate_on_non_extension_side_ok(self) -> None: - """ - Before: - □ - \ - \ - --------->[--------> - - After: - □ - \ - \ - -------------------> - """ - # Setup - design: sc.Design = sc.Design(helices=[sc.Helix(max_offset=100)]) - design.draw_strand(0, 0).extension_5p(5).to(10) - design.draw_strand(0, 10).to(20) - - # Action - design.ligate(0, 10, True) - - # Verify - self.assertEqual(1, len(design.strands)) - actual_substrands = design.strands[0].domains - self.assertEqual(2, len(actual_substrands)) - self.assertEqual(sc.Extension(5), actual_substrands[0]) - self.assertEqual(sc.Domain(0, True, 0, 20), actual_substrands[1]) - - def test_add_full_crossover_extension_ok(self) -> None: - """ - Before: - ↗ - / - / - / - 0 [------- -------- - - 1 <------- -------] - - After: - - ↗ - / - / - / - 0 [------+ +------- - | | - 1 <------+ +------] - """ - # Setup - design: sc.Design = sc.Design( - helices=[sc.Helix(max_offset=100), sc.Helix(max_offset=100)] - ) - design.draw_strand(0, 0).to(16).extension_3p(5) - design.draw_strand(1, 16).to(0) - - # Action - design.add_full_crossover(0, 1, 8, True) - - # Validation - expected_strand_0: sc.Strand = sc.Strand([ - sc.Domain(0, True, 0, 8), - sc.Domain(1, False, 0, 8) - ]) - expected_strand_1: sc.Strand = sc.Strand([ - sc.Domain(1, False, 8, 16), - sc.Domain(0, True, 8, 16), - sc.Extension(5) - ]) - self.assertEqual(2, len(design.strands)) - self.assertIn(expected_strand_0, design.strands) - self.assertIn(expected_strand_1, design.strands) - - def test_add_full_crossover_on_extension_error(self) -> None: - """ - Before: - ↗ - / - / - / - 0 [------- [------> - - 1 <------] <------] - - Error: - ↗ - / - / - / - 0 [------+ +------> - | | - 1 <------+ +------] - """ - design: sc.Design = sc.Design( - helices=[sc.Helix(max_offset=100), sc.Helix(max_offset=100)] - ) - design.draw_strand(0, 0).to(8).extension_3p(5) - design.draw_strand(0, 8).to(16) - design.draw_strand(1, 8).to(0) - design.draw_strand(1, 16).to(8) - - with self.assertRaises(sc.IllegalDesignError): - design.add_full_crossover(0, 1, 8, True) - - def test_add_half_crossover_on_extension_ok(self) -> None: - """ - Before: - □ - \ - \ - 0 -------> - - 1 <------] - - After: - □ - \ - \ - 0 -------+ - | - 1 <------+ - """ - # Setup - design: sc.Design = sc.Design( - helices=[sc.Helix(max_offset=100), sc.Helix(max_offset=100)] - ) - design.draw_strand(0, 0).extension_5p(5).to(8) - design.draw_strand(1, 8).to(0) - - # Action - design.add_half_crossover(0, 1, 7, True) - - # Validation - expected_strand: sc.Strand = sc.Strand([ - sc.Extension(5), - sc.Domain(0, True, 0, 8), - sc.Domain(1, False, 0, 8) - ]) - self.assertEqual(1, len(design.strands)) - self.assertEqual(expected_strand, design.strands[0]) - - def test_add_half_crossover_on_extension_error(self) -> None: - """ - Before: - □ - \ - \ - 0 -------> - - 1 <------] - - Error: - □ - \ - \ - 0 +------> - | - 1 +------] - """ - # Setup - design: sc.Design = sc.Design( - helices=[sc.Helix(max_offset=100), sc.Helix(max_offset=100)] - ) - design.draw_strand(0, 0).extension_5p(5).to(8) - design.draw_strand(1, 8).to(0) - - with self.assertRaises(sc.IllegalDesignError): - design.add_half_crossover(0, 1, 0, True) - - def test_add_half_crossover_on_existing_crossover_should_error_5p_case(self) -> None: - """ - 0 +------] - | - 1 +------> - ^ - error to cross here - v - 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) - - with self.assertRaises(sc.IllegalDesignError): - design.add_half_crossover(1, 2, 0, True) - - def test_add_half_crossover_on_existing_crossover_should_error_3p_case(self) -> None: - """ - 0 <------+ - | - 1 [------+ - ^ - error to cross here - v - 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(1, 0).to(10).cross(0).to(0) - design.draw_strand(2, 10).to(0) - - with self.assertRaises(sc.IllegalDesignError): - design.add_half_crossover(1, 2, 9, True) - - def test_nick_on_extension(self) -> None: - """ - Before: - ↗ - / - / - / - 0 [------- - - After: - ↗ - / - / - / - 0 [-->[--- - """ - # Setup - design: sc.Design = sc.Design(helices=[sc.Helix(max_offset=100), sc.Helix(max_offset=100)]) - design.draw_strand(0, 0).to(8).extension_3p(5) - - # Nick - design.add_nick(0, 4, True) - - # Verification - expected_strand1: sc.Strand = sc.Strand([ - sc.Domain(0, True, 0, 4), - ]) - expected_strand2: sc.Strand = sc.Strand([ - sc.Domain(0, True, 4, 8), - sc.Extension(5) - ]) - self.assertEquals(2, len(design.strands)) - self.assertIn(expected_strand1, design.strands) - self.assertIn(expected_strand2, design.strands) - - -class TestAutocalculatedData(unittest.TestCase): - - def test_helix_min_max_offsets_illegal_explicitly_specified(self) -> None: - helices = [sc.Helix(min_offset=5, max_offset=5)] - with self.assertRaises(sc.IllegalDesignError): - sc.Design(helices=helices, strands=[], grid=sc.square) - - def test_helix_min_max_offsets_illegal_autocalculated(self) -> None: - helices = [sc.Helix(min_offset=5)] - ss = sc.Domain(0, True, 0, 4) - strand = sc.Strand([ss]) - with self.assertRaises(sc.IllegalDesignError): - sc.Design(helices=helices, strands=[strand], grid=sc.square) - - def test_helix_min_max_offsets(self) -> None: - helices = [sc.Helix(), sc.Helix(min_offset=-5), sc.Helix(max_offset=5), - sc.Helix(min_offset=5, max_offset=10)] - ss_0 = sc.Domain(helix=0, forward=True, start=20, end=25) - ss_1 = sc.Domain(helix=1, forward=False, start=-5, end=30) - ss_2 = sc.Domain(helix=2, forward=True, start=0, end=5) - ss_3 = sc.Domain(helix=3, forward=False, start=5, end=10) - strand = sc.Strand([ss_0, ss_1, ss_2, ss_3]) - design = sc.Design(helices=helices, strands=[strand], grid=sc.square) - self.assertEqual(0, design.helices[0].min_offset) - self.assertEqual(25, design.helices[0].max_offset) - self.assertEqual(-5, design.helices[1].min_offset) - self.assertEqual(30, design.helices[1].max_offset) - self.assertEqual(0, design.helices[2].min_offset) - self.assertEqual(5, design.helices[2].max_offset) - self.assertEqual(5, design.helices[3].min_offset) - self.assertEqual(10, design.helices[3].max_offset) - - def test_helix_max_offset(self) -> None: - helices = [sc.Helix(), sc.Helix(max_offset=8), sc.Helix()] - ss_0 = sc.Domain(helix=0, forward=True, start=5, end=10) - ss_1 = sc.Domain(helix=1, forward=False, start=2, end=6) - ss_2 = sc.Domain(helix=2, forward=True, start=0, end=5) - strand = sc.Strand([ss_0, ss_1, ss_2]) - design = sc.Design(helices=helices, strands=[strand], grid=sc.square) - self.assertEqual(10, design.helices[0].max_offset) - self.assertEqual(8, design.helices[1].max_offset) - self.assertEqual(5, design.helices[2].max_offset) - - -class TestSetHelixIdx(unittest.TestCase): - def test_set_helix_idx(self) -> None: - helices = [sc.Helix(max_offset=20), sc.Helix(max_offset=20), sc.Helix(max_offset=20)] - ss_0 = sc.Domain(helix=0, forward=True, start=0, end=6) - ss_1 = sc.Domain(helix=1, forward=True, start=0, end=7) - ss_2 = sc.Domain(helix=2, forward=True, start=0, end=8) - strand = sc.Strand([ss_0, ss_1, ss_2]) - design = sc.Design(helices=helices, strands=[strand], grid=sc.square) - design.set_helix_idx(2, 5) - design.set_helix_idx(1, 3) - design.set_helix_idx(0, 2) - self.assertEqual(3, len(design.helices)) - self.assertTrue(2 in design.helices) - self.assertTrue(3 in design.helices) - self.assertTrue(5 in design.helices) - h2 = design.helices[2] - h3 = design.helices[3] - h5 = design.helices[5] - self.assertEqual(2, h2.idx) - self.assertEqual(3, h3.idx) - self.assertEqual(5, h5.idx) - - ss2 = design.domain_at(2, 0, True) - ss3 = design.domain_at(3, 0, True) - ss5 = design.domain_at(5, 0, True) - self.assertEqual(2, ss2.helix) - self.assertEqual(3, ss3.helix) - self.assertEqual(5, ss5.helix) - self.assertEqual(6, ss2.end) - self.assertEqual(7, ss3.end) - self.assertEqual(8, ss5.end) - - -class TestDesignPitchYawRollOfHelix(unittest.TestCase): - def setUp(self) -> None: - n = 'north' - helix = sc.Helix(max_offset=12, group=n, grid_position=(1, 2), roll=4) - - group_north = sc.HelixGroup(position=sc.Position3D(x=0, y=-200, z=0), grid=sc.square, pitch=12, - yaw=40, roll=32) - self.design = sc.Design(helices=[helix], groups={n: group_north}, strands=[]) - self.helix = helix - - def test_design_pitch_of_helix(self) -> None: - self.assertEqual(12, self.design.pitch_of_helix(self.helix)) - - def test_design_yaw_of_helix(self) -> None: - self.assertEqual(40, self.design.yaw_of_helix(self.helix)) - - def test_design_roll_of_helix(self) -> None: - self.assertEqual(36, self.design.roll_of_helix(self.helix)) - - -class TestHelixGroups(unittest.TestCase): - def setUp(self) -> None: - n = 'north' - e = 'east' - s = 'south' - w = 'west' - helices = [ - sc.Helix(max_offset=20, group=n, grid_position=(1, 1)), # 0 - sc.Helix(max_offset=21, group=n, grid_position=(0, 1)), # 1 - sc.Helix(max_offset=19, group=n, grid_position=(0, 2)), # 2 - sc.Helix(max_offset=18, group=n, grid_position=(1, 2)), # 3 - sc.Helix(max_offset=17, group=n, grid_position=(2, 2)), # 4 - sc.Helix(max_offset=16, group=n, grid_position=(2, 1)), # 5 - sc.Helix(max_offset=24, group=s), # 6 - sc.Helix(max_offset=25, group=s), # 7 - sc.Helix(max_offset=26, group=w, position=sc.Position3D(x=0, y=0, z=0)), # 8 - sc.Helix(max_offset=27, group=w, position=sc.Position3D(x=0, y=2.5, z=0)), # 9 - sc.Helix(idx=13, max_offset=22, group=e), # 13 - sc.Helix(idx=15, max_offset=23, group=e), # 15 - ] - group_north = sc.HelixGroup(position=sc.Position3D(x=0, y=-200, z=0), grid=sc.honeycomb) - group_south = sc.HelixGroup(position=sc.Position3D(x=0, y=70, z=0), helices_view_order=[7, 6], - grid=sc.square) - group_east = sc.HelixGroup(position=sc.Position3D(x=0, y=0, z=100), pitch=45, grid=sc.square) - group_west = sc.HelixGroup() - groups = { - n: group_north, - e: group_east, - s: group_south, - w: group_west, - } - self.design = sc.Design(helices=helices, groups=groups, strands=[]) - self.n = n - self.e = e - self.s = s - self.w = w - - def test_helix_groups(self) -> None: - self._asserts_for_fixture(self.design) - - def test_helix_groups_to_from_JSON(self) -> None: - n = self.n - e = self.e - s = self.s - w = self.w - design_json_str = self.design.to_json() - - design_json_map = json.loads(design_json_str) - groups_map = design_json_map[sc.groups_key] - group_n = groups_map[n] - group_e = groups_map[e] - group_s = groups_map[s] - group_w = groups_map[w] - - pos_n = group_n[sc.position_key] - self.assertAlmostEqual(0, pos_n['x']) - self.assertAlmostEqual(-200, pos_n['y']) - self.assertAlmostEqual(0, pos_n['z']) - - pos_s = group_e[sc.position_key] - self.assertAlmostEqual(0, pos_s['x']) - self.assertAlmostEqual(0, pos_s['y']) - self.assertAlmostEqual(100, pos_s['z']) - - pos_w = group_s[sc.position_key] - self.assertAlmostEqual(0, pos_w['x']) - self.assertAlmostEqual(70, pos_w['y']) - self.assertAlmostEqual(0, pos_w['z']) - - pos_e = group_w[sc.position_key] - self.assertAlmostEqual(0, pos_e['x']) - self.assertAlmostEqual(0, pos_e['y']) - self.assertAlmostEqual(0, pos_e['z']) - - helices_map = design_json_map[sc.helices_key] - self.assertEqual(12, len(helices_map)) - helix0_map = helices_map[0] - helix1_map = helices_map[1] - helix2_map = helices_map[2] - helix3_map = helices_map[3] - helix4_map = helices_map[4] - helix5_map = helices_map[5] - helix6_map = helices_map[6] - helix7_map = helices_map[7] - helix8_map = helices_map[8] - helix9_map = helices_map[9] - helix13_map = helices_map[10] - helix15_map = helices_map[11] - - self.assertEqual(n, helix0_map[sc.group_key]) - self.assertEqual(n, helix1_map[sc.group_key]) - self.assertEqual(n, helix2_map[sc.group_key]) - self.assertEqual(n, helix3_map[sc.group_key]) - self.assertEqual(n, helix4_map[sc.group_key]) - self.assertEqual(n, helix5_map[sc.group_key]) - self.assertEqual(s, helix6_map[sc.group_key]) - self.assertEqual(s, helix7_map[sc.group_key]) - self.assertEqual(w, helix8_map[sc.group_key]) - self.assertEqual(w, helix9_map[sc.group_key]) - self.assertEqual(e, helix13_map[sc.group_key]) - self.assertEqual(e, helix15_map[sc.group_key]) - - self.assertEqual(0, helix0_map[sc.idx_on_helix_key]) - self.assertEqual(1, helix1_map[sc.idx_on_helix_key]) - self.assertEqual(2, helix2_map[sc.idx_on_helix_key]) - self.assertEqual(3, helix3_map[sc.idx_on_helix_key]) - self.assertEqual(4, helix4_map[sc.idx_on_helix_key]) - self.assertEqual(5, helix5_map[sc.idx_on_helix_key]) - self.assertEqual(6, helix6_map[sc.idx_on_helix_key]) - self.assertEqual(7, helix7_map[sc.idx_on_helix_key]) - self.assertEqual(8, helix8_map[sc.idx_on_helix_key]) - self.assertEqual(9, helix9_map[sc.idx_on_helix_key]) - self.assertEqual(13, helix13_map[sc.idx_on_helix_key]) - self.assertEqual(15, helix15_map[sc.idx_on_helix_key]) - - design_from_json = sc.Design.from_scadnano_json_str(design_json_str) - self._asserts_for_fixture(design_from_json) - - def test_helix_groups_fail_nonexistent(self) -> None: - helices = [ - sc.Helix(max_offset=20, group="north"), - sc.Helix(max_offset=21, group="east"), - ] - group_north = sc.HelixGroup(position=sc.Position3D(x=0, y=-200, z=0), grid=sc.honeycomb) - groups = {self.n: group_north} - with self.assertRaises(sc.IllegalDesignError): - sc.Design(helices=helices, groups=groups, strands=[]) - - def _asserts_for_fixture(self, design: sc.Design): - n = self.n - e = self.e - s = self.s - w = self.w - groups = design.groups - if groups is None: - return # this makes MyPy shut up about how groups might be None - - self.assertEqual(4, len(groups)) - - self.assertSequenceEqual([0, 1, 2, 3, 4, 5], groups[n].helices_view_order) - self.assertSequenceEqual([7, 6], groups[s].helices_view_order) - self.assertSequenceEqual([8, 9], groups[w].helices_view_order) - self.assertSequenceEqual([13, 15], groups[e].helices_view_order) - - self.assertEqual(sc.Grid.honeycomb, groups[n].grid) - self.assertEqual(sc.Grid.square, groups[e].grid) - self.assertEqual(sc.Grid.square, groups[s].grid) - self.assertEqual(sc.Grid.none, groups[w].grid) - - self.assertAlmostEqual(0, groups[n].pitch) - self.assertAlmostEqual(45, groups[e].pitch) - self.assertAlmostEqual(0, groups[s].pitch) - self.assertAlmostEqual(0, groups[w].pitch) - - # test auto-assignment of grid_positions based on helices view order - self.assertSequenceEqual([0, 1], design.helices[6].grid_position) - self.assertSequenceEqual([0, 0], design.helices[7].grid_position) - self.assertSequenceEqual([0, 0], design.helices[13].grid_position) - self.assertSequenceEqual([0, 1], design.helices[15].grid_position) - - def test_JSON_bad_uses_groups_and_top_level_grid(self) -> None: - json_str = ''' -{ - "grid": "none", - "groups": { - "north": { - "position": {"x": 0, "y": -200, "z": 0}, - "grid": "honeycomb" - }, - "east": { - "position": {"x": 0, "y": 0, "z": 100}, - "pitch": 45, - "grid": "square" - } - }, - "helices": [ - {"group": "north", "max_offset": 20, "grid_position": [1, 1]}, - {"group": "north", "max_offset": 21, "grid_position": [0, 1]}, - {"group": "east", "max_offset": 22, "grid_position": [0, 13]}, - {"group": "east", "max_offset": 23, "grid_position": [0, 15]} - ], - "strands": [ - { - "color": "#f74308", - "domains": [ - {"helix": 0, "forward": true, "start": 0, "end": 8}, - {"helix": 1, "forward": false, "start": 0, "end": 8} - ] - }, - { - "color": "#57bb00", - "domains": [ - {"helix": 2, "forward": true, "start": 0, "end": 8}, - {"helix": 3, "forward": false, "start": 0, "end": 8} - ] - } - ] -} - ''' - with self.assertRaises(sc.IllegalDesignError): - sc.Design.from_scadnano_json_str(json_str) - - def test_JSON_bad_uses_groups_and_top_level_helices_view_order(self) -> None: - json_str = ''' -{ - "helices_view_order": [3, 2, 1, 0], - "groups": { - "north": { - "position": {"x": 0, "y": -200, "z": 0}, - "grid": "honeycomb" - }, - "east": { - "position": {"x": 0, "y": 0, "z": 100}, - "pitch": 45, - "grid": "square" - } - }, - "helices": [ - {"group": "north", "max_offset": 20, "grid_position": [1, 1]}, - {"group": "north", "max_offset": 21, "grid_position": [0, 1]}, - {"group": "east", "max_offset": 22, "grid_position": [0, 13]}, - {"group": "east", "max_offset": 23, "grid_position": [0, 15]} - ], - "strands": [ - { - "color": "#f74308", - "domains": [ - {"helix": 0, "forward": true, "start": 0, "end": 8}, - {"helix": 1, "forward": false, "start": 0, "end": 8} - ] - }, - { - "color": "#57bb00", - "domains": [ - {"helix": 2, "forward": true, "start": 0, "end": 8}, - {"helix": 3, "forward": false, "start": 0, "end": 8} - ] - } - ] -} - ''' - with self.assertRaises(sc.IllegalDesignError): - sc.Design.from_scadnano_json_str(json_str) - - def test_JSON_bad_no_groups_but_helices_reference_groups(self) -> None: - json_str = ''' -{ - "grid": "square", - "helices": [ - {"group": "north", "max_offset": 20, "grid_position": [0, 0]}, - {"group": "north", "max_offset": 21, "grid_position": [0, 1]}, - {"group": "east", "max_offset": 22, "grid_position": [0, 2]}, - {"group": "east", "max_offset": 23, "grid_position": [0, 3]} - ], - "strands": [ - { - "color": "#f74308", - "domains": [ - {"helix": 0, "forward": true, "start": 0, "end": 8}, - {"helix": 1, "forward": false, "start": 0, "end": 8} - ] - }, - { - "color": "#57bb00", - "domains": [ - {"helix": 2, "forward": true, "start": 0, "end": 8}, - {"helix": 3, "forward": false, "start": 0, "end": 8} - ] - } - ] -} - -''' - with self.assertRaises(sc.IllegalDesignError): - sc.Design.from_scadnano_json_str(json_str) - - -class TestNames(unittest.TestCase): - - def test_strand_domain_names_json(self) -> None: - helices = [sc.Helix(max_offset=100), sc.Helix(max_offset=100), sc.Helix(max_offset=100)] - strand0 = sc.Strand(name='strand0', domains=[ - sc.Domain(0, True, 0, 8, name='domain_forward0'), - sc.Domain(0, True, 8, 16, name='domain_forward1'), - ]) - strand1 = sc.Strand(name='strand1', domains=[ - sc.Domain(0, False, 0, 8, name='domain_reverse0'), - sc.Loopout(3, name='loopout'), - sc.Domain(2, True, 0, 8, name='domain_forward2'), - ]) - strand2 = sc.Strand(domains=[ - sc.Domain(1, True, 0, 8), - sc.Domain(1, True, 8, 16, name='domain_forward0'), - ]) - strands = [strand0, strand1, strand2] - design = sc.Design(helices=helices, strands=strands, grid=sc.square) - - json_map = design.to_json_serializable(suppress_indent=False) - - self.assertIn(sc.strand_name_key, json_map[sc.strands_key][0].keys()) - self.assertIn(sc.strand_name_key, json_map[sc.strands_key][1].keys()) - self.assertNotIn(sc.strand_name_key, json_map[sc.strands_key][2].keys()) - self.assertEqual('strand0', json_map[sc.strands_key][0][sc.strand_name_key]) - self.assertEqual('strand1', json_map[sc.strands_key][1][sc.strand_name_key]) - - self.assertIn(sc.domain_name_key, json_map[sc.strands_key][0]['domains'][0]) - self.assertIn(sc.domain_name_key, json_map[sc.strands_key][0]['domains'][1]) - self.assertEqual('domain_forward0', json_map[sc.strands_key][0]['domains'][0][sc.domain_name_key]) - self.assertEqual('domain_forward1', json_map[sc.strands_key][0]['domains'][1][sc.domain_name_key]) - - self.assertIn(sc.domain_name_key, json_map[sc.strands_key][1]['domains'][0]) - self.assertIn(sc.domain_name_key, json_map[sc.strands_key][1]['domains'][1]) - self.assertIn(sc.domain_name_key, json_map[sc.strands_key][1]['domains'][2]) - self.assertEqual('domain_reverse0', json_map[sc.strands_key][1]['domains'][0][sc.domain_name_key]) - self.assertEqual('loopout', json_map[sc.strands_key][1]['domains'][1][sc.domain_name_key]) - self.assertEqual('domain_forward2', json_map[sc.strands_key][1]['domains'][2][sc.domain_name_key]) - - self.assertIn(sc.domain_name_key, json_map[sc.strands_key][2]['domains'][1]) - self.assertEqual('domain_forward0', json_map[sc.strands_key][2]['domains'][1][sc.domain_name_key]) - - design_from_json = sc.Design.from_scadnano_json_map(json_map) - - self.assertEqual(3, len(design_from_json.strands)) - self.assertEqual('strand0', design_from_json.strands[0].name) - self.assertEqual('strand1', design_from_json.strands[1].name) - self.assertEqual(None, design_from_json.strands[2].name) - - self.assertEqual('domain_forward0', design_from_json.strands[0].domains[0].name) - self.assertEqual('domain_forward1', design_from_json.strands[0].domains[1].name) - - self.assertEqual('domain_reverse0', design_from_json.strands[1].domains[0].name) - self.assertEqual('loopout', design_from_json.strands[1].domains[1].name) - self.assertEqual('domain_forward2', design_from_json.strands[1].domains[2].name) - - self.assertEqual(None, design_from_json.strands[2].domains[0].name) - self.assertEqual('domain_forward0', design_from_json.strands[2].domains[1].name) - - def test_strand_names_can_be_nonunique(self) -> None: - helices = [sc.Helix(max_offset=100)] - strand0 = sc.Strand(name='strand0', domains=[ - sc.Domain(0, True, 0, 8, name='domain_forward0'), - sc.Domain(0, True, 8, 16, name='domain_forward1'), - ]) - strand1 = sc.Strand(name='strand0', domains=[ - sc.Domain(0, False, 0, 8, name='domain_reverse0'), - sc.Domain(0, False, 8, 16, name='domain_reverse1'), - ]) - strands = [strand0, strand1] - sc.Design(helices=helices, strands=strands, grid=sc.square) - - -class TestJSON(unittest.TestCase): - - def test_grid_design_level_converted_to_enum_from_string(self) -> None: - # reproduces an error where the grid was stored as a string instead of the Grid enum type - json_str = ''' -{ - "version": "0.14.0", - "grid": "square", - "helices": [ - {"grid_position": [0, 0]}, - {"grid_position": [0, 1]} - ], - "strands": [] -} - ''' - design = sc.Design.from_scadnano_json_str(json_str) - grid = design.grid - self.assertTrue(type(grid) is sc.Grid) - self.assertEqual(sc.Grid.square, grid) - - def test_grid_helix_group_level_converted_to_enum_from_string(self) -> None: - # reproduces an error where the grid was stored as a string instead of the Grid enum type - json_str = ''' -{ - "version": "0.15.0", - "groups": { - "north": { - "position": {"x": 0, "y": -10, "z": 0}, - "grid": "honeycomb" - }, - "east": { - "position": {"x": 0, "y": 0, "z": 10}, - "grid": "square" - }, - "south": { - "position": {"x": 0, "y": 10, "z": 0}, - "grid": "hex" - }, - "west": { - "position": {"x": 0, "y": 0, "z": -10}, - "grid": "none" - } - }, - "helices": [ - {"group": "north", "max_offset": 20, "grid_position": [0, 0], "idx": 0}, - {"group": "north", "max_offset": 21, "grid_position": [1, 0], "idx": 1}, - {"group": "north", "max_offset": 19, "grid_position": [1, 1], "idx": 2}, - {"group": "north", "max_offset": 18, "grid_position": [0, 1], "idx": 3}, - {"group": "north", "max_offset": 17, "grid_position": [-1, 1], "idx": 4}, - {"group": "north", "max_offset": 16, "grid_position": [-1, 0], "idx": 5}, - {"group": "south", "max_offset": 24, "grid_position": [0, 1], "idx": 6}, - {"group": "south", "max_offset": 25, "grid_position": [0, 0], "idx": 7}, - {"group": "west", "max_offset": 26, "position": {"x": 0, "y": 0, "z": 0}, "idx": 8}, - {"group": "west", "max_offset": 27, "position": {"x": 0, "y": 3, "z": 0}, "idx": 9}, - {"group": "east", "max_offset": 22, "grid_position": [0, 0], "idx": 13}, - {"group": "east", "max_offset": 23, "grid_position": [0, 1], "idx": 15} - ], - "strands": [] -} - ''' - design = sc.Design.from_scadnano_json_str(json_str) - - grid_n = design.groups['north'].grid - grid_e = design.groups['east'].grid - grid_s = design.groups['south'].grid - grid_w = design.groups['west'].grid - for grid in [grid_n, grid_e, grid_s, grid_w]: - self.assertTrue(type(grid) is sc.Grid) - - self.assertEqual(sc.Grid.honeycomb, grid_n) - self.assertEqual(sc.Grid.square, grid_e) - self.assertEqual(sc.Grid.hex, grid_s) - self.assertEqual(sc.Grid.none, grid_w) - - def test_legacy_idt_name_import__no_strand_name(self) -> None: - # tests proper importing of old format when name was a subfield of idt; - # ensures if that exists and no Strand.name field exists, the idt.name is used as Strand.name - json_str = ''' - { - "version": "0.14.0", - "grid": "square", - "helices": [ - {"grid_position": [0, 0]}, - {"grid_position": [0, 1]} - ], - "strands": [ - { - "color": "#f74308", - "sequence": "TATTATAGTCTTACCCTGAC", - "idt": {"name": "staple1", "scale": "100nm", "purification": "HPLC", "plate": "plate1", "well": "A1"}, - "domains": [ - {"helix": 0, "forward": true, "start": 0, "end": 10}, - {"helix": 1, "forward": false, "start": 0, "end": 10} - ] - } - ] - } - ''' - design = sc.Design.from_scadnano_json_str(json_str) - self.assertEqual(1, len(design.strands)) - strand = design.strands[0] - self.assertEqual('staple1', strand.name) - self.assertEqual('100nm', strand.idt.scale) - self.assertEqual('HPLC', strand.idt.purification) - self.assertEqual('plate1', strand.idt.plate) - self.assertEqual('A1', strand.idt.well) - - def test_legacy_idt_name_import__strand_name_exists(self) -> None: - # tests proper importing of old format when name was a subfield of idt; - # ensures if both exist, we use Strand.name - json_str = ''' - { - "version": "0.14.0", - "grid": "square", - "helices": [ - {"grid_position": [0, 0]}, - {"grid_position": [0, 1]} - ], - "strands": [ - { - "name": "staple1 strand level", - "color": "#f74308", - "sequence": "TATTATAGTCTTACCCTGAC", - "idt": {"name": "staple1", "scale": "100nm", "purification": "HPLC", "plate": "plate1", "well": "A1"}, - "domains": [ - {"helix": 0, "forward": true, "start": 0, "end": 10}, - {"helix": 1, "forward": false, "start": 0, "end": 10} - ] - } - ] - } - ''' - design = sc.Design.from_scadnano_json_str(json_str) - self.assertEqual(1, len(design.strands)) - strand = design.strands[0] - self.assertEqual('staple1 strand level', strand.name) - - def test_Helix_major_tick_start_default_min_offset(self) -> None: - helices = [ - sc.Helix(min_offset=10, max_offset=100), - sc.Helix(max_offset=100), - sc.Helix(major_tick_start=15), - ] - design = sc.Design(helices=helices, strands=[], grid=sc.square) - self.assertEqual(10, design.helices[0].major_tick_start) - self.assertEqual(0, design.helices[1].major_tick_start) - self.assertEqual(15, design.helices[2].major_tick_start) - - design_json_map = design.to_json_serializable(suppress_indent=False) - self.assertNotIn(sc.major_tick_start_key, design_json_map['helices'][0]) - self.assertNotIn(sc.major_tick_start_key, design_json_map['helices'][1]) - self.assertIn(sc.major_tick_start_key, design_json_map['helices'][2]) - self.assertEqual(15, design_json_map['helices'][2][sc.major_tick_start_key]) - - # this isn't related to major_tick_start, but it failed for some reason so let's check it - self.assertIn(sc.grid_position_key, design_json_map['helices'][0]) - self.assertIn(sc.grid_position_key, design_json_map['helices'][1]) - self.assertIn(sc.grid_position_key, design_json_map['helices'][2]) - self.assertSequenceEqual([0, 0], design_json_map['helices'][0][sc.grid_position_key]) - self.assertSequenceEqual([0, 1], design_json_map['helices'][1][sc.grid_position_key]) - self.assertSequenceEqual([0, 2], design_json_map['helices'][2][sc.grid_position_key]) - - design_json_str = json.dumps(design_json_map) - design = sc.Design.from_scadnano_json_str(design_json_str) - self.assertEqual(10, design.helices[0].major_tick_start) - self.assertEqual(0, design.helices[1].major_tick_start) - self.assertEqual(15, design.helices[2].major_tick_start) - - def test_Helix_major_tick_periodic_distances(self) -> None: - grid = sc.square - helices = [ - sc.Helix(major_tick_start=10, max_offset=30, major_tick_distance=5), - sc.Helix(major_tick_start=10, max_offset=30, major_tick_periodic_distances=[2, 3]), - sc.Helix(major_tick_start=10, max_offset=30, major_ticks=[10, 20, 30]), - sc.Helix(major_tick_start=10, max_offset=30), - sc.Helix(max_offset=30), - ] - design = sc.Design(helices=helices, strands=[], grid=grid) - self.assertEqual(10, design.helices[0].major_tick_start) - - self.assertSequenceEqual([10, 15, 20, 25, 30], design.helices[0].calculate_major_ticks(grid)) - self.assertSequenceEqual([10, 12, 15, 17, 20, 22, 25, 27, 30], - design.helices[1].calculate_major_ticks(grid)) - self.assertSequenceEqual([10, 20, 30], design.helices[2].calculate_major_ticks(grid)) - self.assertSequenceEqual([10, 18, 26], design.helices[3].calculate_major_ticks(grid)) - self.assertSequenceEqual([0, 8, 16, 24], design.helices[4].calculate_major_ticks(grid)) - - design_json_map = design.to_json_serializable(suppress_indent=False) - - h0 = design_json_map['helices'][0] - self.assertNotIn(sc.major_ticks_key, h0) - self.assertNotIn(sc.major_tick_periodic_distances_key, h0) - self.assertIn(sc.major_tick_distance_key, h0) - self.assertEqual(5, h0[sc.major_tick_distance_key]) - - h1 = design_json_map['helices'][1] - self.assertNotIn(sc.major_ticks_key, h1) - self.assertIn(sc.major_tick_periodic_distances_key, h1) - self.assertNotIn(sc.major_tick_distance_key, h1) - self.assertSequenceEqual([2, 3], h1[sc.major_tick_periodic_distances_key]) - - h2 = design_json_map['helices'][2] - self.assertIn(sc.major_ticks_key, h2) - self.assertNotIn(sc.major_tick_distance_key, h2) - self.assertNotIn(sc.major_tick_periodic_distances_key, h2) - self.assertSequenceEqual([10, 20, 30], h2[sc.major_ticks_key]) - - h3 = design_json_map['helices'][3] - self.assertNotIn(sc.major_ticks_key, h3) - self.assertNotIn(sc.major_tick_distance_key, h3) - self.assertNotIn(sc.major_tick_periodic_distances_key, h3) - self.assertIn(sc.major_tick_start_key, h3) - - h4 = design_json_map['helices'][4] - self.assertNotIn(sc.major_ticks_key, h4) - self.assertNotIn(sc.major_tick_distance_key, h4) - self.assertNotIn(sc.major_tick_periodic_distances_key, h4) - self.assertNotIn(sc.major_tick_start_key, h4) - - def test_default_helices_view_order_with_nondefault_helix_idxs_in_default_order(self) -> None: - helices = [sc.Helix(idx=1, max_offset=100), sc.Helix(idx=3, max_offset=100)] - design = sc.Design(helices=helices, strands=[]) - self.assertListEqual([1, 3], design.helices_view_order) - - # [1, 3] is default so json should not contain key - design_json_ser = design.to_json_serializable(suppress_indent=False) - self.assertFalse(sc.helices_view_order_key in design_json_ser) - - def test_default_helices_view_order_with_nondefault_helix_idxs_in_nondefault_order(self) -> None: - helices = [sc.Helix(idx=1, max_offset=100), sc.Helix(idx=3, max_offset=100)] - design = sc.Design(helices=helices, strands=[], helices_view_order=[3, 1]) - self.assertListEqual([3, 1], design.helices_view_order) - - # [1, 3] is default so json should not contain key - design_json_ser = design.to_json_serializable(suppress_indent=False) - actual_view_order = design_json_ser[sc.helices_view_order_key] - self.assertListEqual([3, 1], actual_view_order) - - def test_strand_labels(self) -> None: - helices = [sc.Helix(max_offset=100), sc.Helix(max_offset=100)] - strand0_expected = sc.Strand([sc.Domain(0, True, 0, 10)], label={ - 'name': 'strand 0', - 'num_domains': 1, - }) - strand1_expected = sc.Strand([sc.Domain(0, False, 0, 10), sc.Domain(1, True, 0, 10)], label={ - 'name': 'strand 1', - 'num_domains': 2, - }) - strands = [strand0_expected, strand1_expected] - design = sc.Design(helices=helices, strands=strands, grid=sc.square) - json_str = design.to_json() - design_from_json = sc.Design.from_scadnano_json_str(json_str) - strand0 = design_from_json.strands[0] - strand1 = design_from_json.strands[1] - self.assertDictEqual(strand0_expected.label, strand0.label) - self.assertDictEqual(strand1_expected.label, strand1.label) - - def test_strand_idt(self) -> None: - helices = [sc.Helix(max_offset=100), sc.Helix(max_offset=100)] - idt = sc.IDTFields(scale='25nm', purification='HPLC', plate='plate1', well='A2') - strand0_expected = sc.Strand([sc.Domain(0, True, 0, 10)], name='strand1', idt=idt) - strands = [strand0_expected] - design = sc.Design(helices=helices, strands=strands, grid=sc.square) - json_str = design.to_json() - design_from_json = sc.Design.from_scadnano_json_str(json_str) - strand0 = design_from_json.strands[0] - self.assertEqual(strand0_expected.idt, strand0.idt) - self.assertEqual('strand1', strand0.idt_export_name()) - self.assertEqual('25nm', strand0.idt.scale) - self.assertEqual('HPLC', strand0.idt.purification) - self.assertEqual('plate1', strand0.idt.plate) - self.assertEqual('A2', strand0.idt.well) - - def test_domain_labels(self) -> None: - helices = [sc.Helix(max_offset=100), sc.Helix(max_offset=100)] - dom00_expected = sc.Domain(0, True, 0, 10, label='domain 00') - dom10_expected = sc.Domain(0, False, 0, 10) - dom11_expected = sc.Domain(1, True, 0, 10, label='domain 11') - strand0 = sc.Strand([dom00_expected]) - strand1 = sc.Strand([dom10_expected, dom11_expected]) - strands = [strand0, strand1] - design = sc.Design(helices=helices, strands=strands, grid=sc.square) - json_str = design.to_json() - design_from_json = sc.Design.from_scadnano_json_str(json_str) - dom00 = design_from_json.strands[0].domains[0] - dom10 = design_from_json.strands[1].domains[0] - dom11 = design_from_json.strands[1].domains[1] - self.assertEqual(dom00_expected.label, dom00.label) - self.assertIsNone(dom10.label) - self.assertEqual(dom11_expected.label, dom11.label) - - def test_nondefault_geometry(self) -> None: - geometry_expected = sc.Geometry(rise_per_base_pair=10.0, helix_radius=4.0, bases_per_turn=11.0, - minor_groove_angle=10.0, - inter_helix_gap=5.0) - design = sc.Design(helices=[], strands=[], geometry=geometry_expected) - json_str = design.to_json() - design_from_json = sc.Design.from_scadnano_json_str(json_str) - geometry_actual = design_from_json.geometry - self.assertAlmostEqual(geometry_expected.rise_per_base_pair, geometry_actual.rise_per_base_pair) - self.assertAlmostEqual(geometry_expected.helix_radius, geometry_actual.helix_radius) - self.assertAlmostEqual(geometry_expected.bases_per_turn, geometry_actual.bases_per_turn) - self.assertAlmostEqual(geometry_expected.minor_groove_angle, geometry_actual.minor_groove_angle) - self.assertAlmostEqual(geometry_expected.inter_helix_gap, geometry_actual.inter_helix_gap) - - def test_nondefault_geometry_some_default(self) -> None: - geometry_expected = sc.Geometry(rise_per_base_pair=10.0, minor_groove_angle=10.0, inter_helix_gap=5.0) - design = sc.Design(helices=[], strands=[], geometry=geometry_expected) - json_str = design.to_json() - design_from_json = sc.Design.from_scadnano_json_str(json_str) - geometry_actual = design_from_json.geometry - self.assertAlmostEqual(geometry_expected.rise_per_base_pair, geometry_actual.rise_per_base_pair) - self.assertAlmostEqual(geometry_expected.helix_radius, geometry_actual.helix_radius) - self.assertAlmostEqual(geometry_expected.bases_per_turn, geometry_actual.bases_per_turn) - self.assertAlmostEqual(geometry_expected.minor_groove_angle, geometry_actual.minor_groove_angle) - self.assertAlmostEqual(geometry_expected.inter_helix_gap, geometry_actual.inter_helix_gap) - - def test_lack_of_NoIndent_on_helix_if_position_or_major_ticks_present(self) -> None: - helices = [sc.Helix(position=sc.Position3D(0, 0, 0))] - strands = [] - design = sc.Design(helices=helices, strands=strands) - json_map = design.to_json_serializable(suppress_indent=True) - helix_json = json_map[sc.helices_key][0] - self.assertTrue(isinstance(helix_json, sc.NoIndent)) - # self.assertTrue(isinstance(helix_json[sc.position_key], sc.NoIndent)) - - def test_NoIndent_on_helix_without_position_or_major_ticks_present(self) -> None: - helices = [sc.Helix()] - strands = [] - # will only be NoIndent if we don't have a Helix.position field, so make sure one isn't created - # by choosing a grid - design = sc.Design(helices=helices, strands=strands, grid=sc.square) - json_map = design.to_json_serializable(suppress_indent=True) - helix_json = json_map[sc.helices_key][0] - self.assertTrue(isinstance(helix_json, sc.NoIndent)) - - def test_error_when_grid_missing(self) -> None: - json_str = """ - { - "helices": [{"grid_position": [0,0]}], - "strands": [ - { - "color": 26316, - "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] - } - ] - } - """ - with self.assertRaises(sc.IllegalDesignError) as ex: - sc.Design.from_scadnano_json_str(json_str) - msg = ex.exception.args[0] - self.assertTrue('grid' in msg) - - def test_error_when_domain_helix_missing(self) -> None: - json_str = """ - { - "grid": "square", - "helices": [{"grid_position": [0,0]}], - "strands": [ - { - "color": 26316, - "domains": [ {"forward": true, "start": 0, "end": 32} ] - } - ] - } - """ - with self.assertRaises(sc.IllegalDesignError) as ex: - sc.Design.from_scadnano_json_str(json_str) - msg = ex.exception.args[0] - self.assertTrue('helix' in msg) - - def test_error_when_domain_forward_and_right_missing(self) -> None: - json_str = """ - { - "grid": "square", - "helices": [{"grid_position": [0,0]}], - "strands": [ - { - "color": 26316, - "domains": [ {"helix": 0, "start": 0, "end": 32} ] - } - ] - } - """ - with self.assertRaises(sc.IllegalDesignError) as ex: - sc.Design.from_scadnano_json_str(json_str) - msg = ex.exception.args[0] - self.assertTrue('forward' in msg) - self.assertTrue('right' in msg) - - def test_error_when_domain_start_missing(self) -> None: - json_str = """ - { - "grid": "square", - "helices": [{"grid_position": [0,0]}], - "strands": [ - { - "color": 26316, - "domains": [ {"helix": 0, "forward": true, "end": 32} ] - } - ] - } - """ - with self.assertRaises(sc.IllegalDesignError) as ex: - sc.Design.from_scadnano_json_str(json_str) - msg = ex.exception.args[0] - self.assertTrue('start' in msg) - - def test_error_when_domain_end_missing(self) -> None: - json_str = """ - { - "grid": "square", - "helices": [{"grid_position": [0,0]}], - "strands": [ - { - "color": 26316, - "domains": [ {"helix": 0, "forward": true, "start": 0 } ] - } - ] - } - """ - with self.assertRaises(sc.IllegalDesignError) as ex: - sc.Design.from_scadnano_json_str(json_str) - msg = ex.exception.args[0] - self.assertTrue('end' in msg) - - def test_error_when_strands_missing(self) -> None: - json_str = """ - { - "grid": "square", - "helices": [{"grid_position": [0,0]}] - } - """ - with self.assertRaises(sc.IllegalDesignError) as ex: - sc.Design.from_scadnano_json_str(json_str) - msg = ex.exception.args[0] - self.assertTrue('strands' in msg) - - def test_legacy_right_key(self) -> None: - json_str = """ - { - "grid": "square", - "helices": [{"grid_position": [0,0]}], - "strands": [ - { - "color": 26316, - "domains": [ {"helix": 0, "right": true, "start": 0, "end": 5 } ] - } - ] - } - """ - d = sc.Design.from_scadnano_json_str(json_str) - self.assertEqual(True, d.strands[0].domains[0].forward) - - def test_legacy_dna_sequence_key(self) -> None: - json_str = """ - { - "grid": "square", - "helices": [{"grid_position": [0,0]}], - "strands": [ - { - "color": 26316, - "dna_sequence": "ACGTA", - "domains": [ {"helix": 0, "right": true, "start": 0, "end": 5 } ] - } - ] - } - """ - d = sc.Design.from_scadnano_json_str(json_str) - self.assertEqual("ACGTA", d.strands[0].dna_sequence) - - def test_legacy_substrands_key(self) -> None: - json_str = """ - { - "grid": "square", - "helices": [{"grid_position": [0,0]}], - "strands": [ - { - "color": 26316, - "substrands": [ {"helix": 0, "forward": true, "start": 0, "end": 5 } ] - } - ] - } - """ - d = sc.Design.from_scadnano_json_str(json_str) - self.assertEqual(0, d.strands[0].domains[0].helix) - self.assertEqual(True, d.strands[0].domains[0].forward) - self.assertEqual(0, d.strands[0].domains[0].start) - self.assertEqual(5, d.strands[0].domains[0].end) - - def test_color_specified_with_integer(self) -> None: - # addresses https://github.com/UC-Davis-molecular-computing/scadnano-python-package/issues/58 - # 0066cc hex is 26316 decimal - json_str = """ - { - "grid": "square", - "helices": [{"grid_position": [0,0]}], - "strands": [ - { - "color": 26316, - "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] - } - ] - } - """ - d = sc.Design.from_scadnano_json_str(json_str) - expected_color_hex = '#0066cc' - actual_color_hex = d.strands[0].color.to_json_serializable(False) - self.assertEqual(expected_color_hex, actual_color_hex) - - def test_single_helix_group_and_helices_specify_pitch_and_yaw(self) -> None: - json_str = """ - { - "helices": [ - { - "group": "north", - "position": {"x": 1, "y": 2, "z": 3}, - "pitch": 4, - "roll": 5, - "yaw": 6 - }, - { - "group": "north", - "position": {"x": 3, "y": 2, "z": 3}, - "roll": 15 - } - ], - "groups": { - "north": { - "position": {"x": 0, "y": -200, "z": 0}, - "pitch": 21, - "yaw": 13, - "grid": "none" - } - }, - "strands": [ - { - "color": "#0066cc", - "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] - } - ] - } - """ - d = sc.Design.from_scadnano_json_str(json_str) - helix0 = d.helices[0] - helix1 = d.helices[1] - self.assertEqual(sc.Position3D(1, 2, 3), helix0.position) - self.assertEqual(5, helix0.roll) - # Helix 0 should have been moved to a new helix group - pitch_25_yaw_19_group_name = f'pitch_25_yaw_19' - pitch_25_yaw_19_group = d.groups[pitch_25_yaw_19_group_name] - self.assertEqual(25, pitch_25_yaw_19_group.pitch) - self.assertEqual(19, pitch_25_yaw_19_group.yaw) - self.assertEqual(pitch_25_yaw_19_group_name, helix0.group) - self.assertEqual(sc.Position3D(3, 2, 3), helix1.position) - self.assertEqual(15, helix1.roll) - self.assertEqual("north", helix1.group) - self.assertEqual(2, len(d.groups)) - - def test_only_individual_helices_specify_pitch_and_yaw(self) -> None: - json_str = """ - { - "helices": [ - { - "group": "north", - "position": {"x": 1, "y": 2, "z": 3}, - "pitch": 25, - "yaw": 19, - "roll": 5 - }, - { - "group": "north", - "position": {"x": 3, "y": 2, "z": 3}, - "pitch": 21, - "yaw": 13, - "roll": 15 - } - ], - "groups": { - "north": { - "position": {"x": 0, "y": -200, "z": 0}, - "grid": "none" - } - }, - "strands": [ - { - "color": "#0066cc", - "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] - } - ] - } - """ - d = sc.Design.from_scadnano_json_str(json_str) - helix0 = d.helices[0] - helix1 = d.helices[1] - - # Helix 0 should have been moved to a new helix group - pitch_25_yaw_19_group_name = f'pitch_25.0_yaw_19.0' - pitch_25_yaw_19_group = d.groups[pitch_25_yaw_19_group_name] - self.assertEqual(sc.Position3D(1, 2, 3), helix0.position) - self.assertEqual(25, pitch_25_yaw_19_group.pitch) - self.assertEqual(19, pitch_25_yaw_19_group.yaw) - self.assertEqual(5, helix0.roll) - self.assertEqual(pitch_25_yaw_19_group_name, helix0.group) - - # Helix 1 should have been moved to a new helix group - pitch_21_yaw_13_group_name = f'pitch_21.0_yaw_13.0' - pitch_21_yaw_13_group = d.groups[pitch_21_yaw_13_group_name] - self.assertEqual(sc.Position3D(3, 2, 3), helix1.position) - self.assertEqual(21, pitch_21_yaw_13_group.pitch) - self.assertEqual(13, pitch_21_yaw_13_group.yaw) - self.assertEqual(15, helix1.roll) - self.assertEqual(pitch_21_yaw_13_group_name, helix1.group) - - self.assertEqual(3, len(d.groups)) - - def test_only_helix_groups_specify_pitch_and_yaw(self) -> None: - json_str = """ - { - "helices": [ - { - "group": "north", - "position": {"x": 1, "y": 2, "z": 3}, - "roll": 5 - }, - { - "group": "south", - "position": {"x": 3, "y": 2, "z": 3}, - "roll": 15 - } - ], - "groups": { - "north": { - "position": {"x": 0, "y": -200, "z": 0}, - "pitch": 21, - "yaw": 13, - "grid": "none" - }, - "south": { - "position": {"x": 0, "y": -400, "z": 0}, - "pitch": 23, - "yaw": 98, - "grid": "none" - } - }, - "strands": [ - { - "color": "#0066cc", - "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] - } - ] - } - """ - d = sc.Design.from_scadnano_json_str(json_str) - helix0 = d.helices[0] - helix1 = d.helices[1] - - north_str = 'north' - south_str = 'south' - north_group = d.groups[north_str] - south_group = d.groups[south_str] - self.assertEqual(2, len(d.groups)) - - self.assertEqual(sc.Position3D(1, 2, 3), helix0.position) - self.assertEqual(5, helix0.roll) - self.assertEqual(21, north_group.pitch) - self.assertEqual(13, north_group.yaw) - self.assertEqual(north_str, helix0.group) - - self.assertEqual(sc.Position3D(3, 2, 3), helix1.position) - self.assertEqual(15, helix1.roll) - self.assertEqual(23, south_group.pitch) - self.assertEqual(98, south_group.yaw) - self.assertEqual(south_str, helix1.group) - - def test_both_helix_groups_and_helices_do_not_specify_pitch_nor_yaw(self) -> None: - json_str = """ - { - "helices": [ - { - "group": "north", - "position": {"x": 1, "y": 2, "z": 3}, - "roll": 5 - }, - { - "group": "south", - "position": {"x": 3, "y": 2, "z": 3}, - "roll": 15 - } - ], - "groups": { - "north": { - "position": {"x": 0, "y": -200, "z": 0}, - "grid": "none" - }, - "south": { - "position": {"x": 0, "y": -400, "z": 0}, - "grid": "none" - } - }, - "strands": [ - { - "color": "#0066cc", - "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] - } - ] - } - """ - d = sc.Design.from_scadnano_json_str(json_str) - helix0 = d.helices[0] - helix1 = d.helices[1] - - north_str = 'north' - south_str = 'south' - north_group = d.groups[north_str] - south_group = d.groups[south_str] - self.assertEqual(2, len(d.groups)) - - self.assertEqual(sc.Position3D(1, 2, 3), helix0.position) - self.assertEqual(5, helix0.roll) - self.assertEqual(0, north_group.pitch) - self.assertEqual(0, north_group.yaw) - self.assertEqual(north_str, helix0.group) - - self.assertEqual(sc.Position3D(3, 2, 3), helix1.position) - self.assertEqual(15, helix1.roll) - self.assertEqual(0, south_group.pitch) - self.assertEqual(0, south_group.yaw) - self.assertEqual(south_str, helix1.group) - - def test_multiple_helix_groups_helices_specify_pitch_and_yaw(self) -> None: - json_str = """ - { - "helices": [ - { - "group": "north", - "position": {"x": 1, "y": 2, "z": 3}, - "pitch": 4, - "roll": 5, - "yaw": 6 }, - { - "group": "south", - "position": {"x": 3, "y": 2, "z": 3}, - "roll": 15 - } - ], - "groups": { - "north": { - "position": {"x": 0, "y": -200, "z": 0}, - "pitch": 21, - "yaw": 13, - "grid": "none" - }, - "south": { - "position": {"x": 0, "y": -400, "z": 0}, - "pitch": 23, - "yaw": 98, - "grid": "none" - } - }, - "strands": [ - { - "color": "#0066cc", - "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] - } - ] - } - """ - d = sc.Design.from_scadnano_json_str(json_str) - helix0 = d.helices[0] - helix1 = d.helices[1] - - north_str = 'north' - south_str = 'south' - north_group = d.groups[north_str] - south_group = d.groups[south_str] - self.assertEqual(2, len(d.groups)) - - self.assertEqual(sc.Position3D(1, 2, 3), helix0.position) - self.assertEqual(5, helix0.roll) - self.assertEqual(25, north_group.pitch) - self.assertEqual(19, north_group.yaw) - self.assertEqual(north_str, helix0.group) - - self.assertEqual(sc.Position3D(3, 2, 3), helix1.position) - self.assertEqual(15, helix1.roll) - self.assertEqual(23, south_group.pitch) - self.assertEqual(98, south_group.yaw) - self.assertEqual(south_str, helix1.group) - - def test_multiple_helix_groups_helices_specify_pitch_and_yaw_invalid(self) -> None: - json_str = """ - { - "helices": [ - { - "group": "north", - "position": {"x": 1, "y": 2, "z": 3}, - "pitch": 4, - "roll": 5, - "yaw": 6 - }, - { - "group": "north", - "position": {"x": 3, "y": 2, "z": 3}, - "pitch": 10, - "roll": 5, - "yaw": 84 - } - ], - "groups": { - "north": { - "position": {"x": 0, "y": -200, "z": 0}, - "grid": "none" - }, - "east": { - "position": {"x": 0, "y": 0, "z": 100}, - "pitch": 45, - "grid": "square" - } - }, - "strands": [ - { - "color": "#0066cc", - "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] - } - ] - } - """ - # Should fail because multiple helices in same helix group are non-parallel - with self.assertRaises(sc.IllegalDesignError): - sc.Design.from_scadnano_json_str(json_str) - - def test_position_specified_with_origin_keyword(self) -> None: - # addresses https://github.com/UC-Davis-molecular-computing/scadnano-python-package/issues/59 - json_str = """ - { - "grid": "none", - "helices": [{ - "origin": {"x": 1, "y": 2, "z": 3}, - "pitch": 4, - "roll": 5, - "yaw": 6 - }], - "strands": [ - { - "color": "#0066cc", - "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] - } - ] - } - """ - d = sc.Design.from_scadnano_json_str(json_str) - expected_position = sc.Position3D(1, 2, 3) - expected_pitch = 4 - expected_roll = 5 - expected_yaw = 6 - actual_position = d.helices[0].position - expected_group_name = f'pitch_{expected_pitch}_yaw_{expected_yaw}' - expected_group = d.groups[expected_group_name] - actual_pitch = expected_group.pitch - actual_roll = d.helices[0].roll - actual_yaw = expected_group.yaw - self.assertEqual(expected_position, actual_position) - self.assertEqual(expected_pitch, actual_pitch) - self.assertEqual(expected_roll, actual_roll) - self.assertEqual(expected_yaw, actual_yaw) - self.assertEqual(expected_group_name, d.helices[0].group) - - def test_json_tristan_example_issue_32(self) -> None: - json_str = """ - { - "version": "0.3.0", - "grid": "square", - "helices": [ - {"grid_position": [0, 0]}, - {"max_offset": 32, "grid_position": [0, 1]} - ], - "strands": [ - { - "color": "#0066cc", - "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ], - "is_scaffold": true - } - ] - } - """ - sc.Design.from_scadnano_json_str(json_str) - - def test_to_json__hairpin(self) -> None: - """ - 01234 - AAACC # helix 0 going forward - \ - T # loopout - G # loopout - C # loopout - A # loopout - C # loopout - / - TTTGG # helix 0 going reverse - """ - ss_f = sc.Domain(helix=0, forward=True, start=0, end=5) - loop = sc.Loopout(length=5) - ss_r = sc.Domain(helix=0, forward=False, start=0, end=5) - strand_forward = sc.Strand([ss_f, loop, ss_r]) - design = sc.Design(strands=[strand_forward], grid=sc.square) - design.assign_dna(strand_forward, 'AAACC TGCAC') - design.to_json() - # should be no error getting here - - def test_to_json__roll(self) -> None: - helix = sc.Helix(roll=90) - ss_f = sc.Domain(helix=0, forward=True, start=0, end=5) - ss_r = sc.Domain(helix=0, forward=False, start=0, end=5) - strand_f = sc.Strand([ss_f]) - strand_r = sc.Strand([ss_r]) - design = sc.Design(helices=[helix], strands=[strand_f, strand_r], grid=sc.square) - design.to_json() - # should be no error getting here - - def test_from_json_extension_design(self) -> None: - json_str = """ - { - "version": "0.17.3", - "grid": "square", - "helices": [ - {"grid_position": [0, 0], "max_offset": 100} - ], - "strands": [ - { - "domains": [ - {"helix": 0, "forward": true, "start": 0, "end": 10}, - {"extension_num_bases": 5, "display_length": 1.4, "display_angle": 50.0} - ], - "is_scaffold": true - } - ] - } - """ - design = sc.Design.from_scadnano_json_str(json_str) - self.assertEqual( - sc.Extension(5, display_length=1.4, display_angle=50.0), - design.strands[0].domains[1]) - - def test_to_json_extension_design__extension(self) -> None: - # Setup - design = sc.Design(helices=[sc.Helix(max_offset=100)], strands=[], grid=sc.square) - design.draw_strand(0, 0).to(10).extension_3p(5) - - # Action - result = design.to_json() - - # Verify - document = json.loads(result) - self.assertEqual(2, len(document["strands"][0]["domains"])) - self.assertIn(sc.extension_key, document["strands"][0]["domains"][1]) - self.assertEqual(5, document["strands"][0]["domains"][1][sc.extension_key]) - - -class TestIllegalStructuresPrevented(unittest.TestCase): - - # def test_to_json__error_if_DNAOrigamiDesign_no_scaffold(self) -> None: - # # we are allowed to delay assigning a scaffold to a DNAOrigamiDesign, - # # but to_json should fail - # st_l = sc.Strand([ - # sc.Substrand(1, True, 0, 8, insertions=[(4, 2)]), - # sc.Substrand(0, False, 0, 8, deletions=[3]), - # ]) - # st_r = sc.Strand([ - # sc.Substrand(0, False, 8, 16), - # sc.Substrand(1, True, 8, 16), - # ]) - # scaf = sc.Strand([ - # sc.Substrand(1, False, 0, 8, insertions=[(4, 2)]), - # sc.Substrand(0, True, 0, 16, deletions=[3]), - # sc.Loopout(3), - # sc.Substrand(1, False, 8, 16, deletions=[]), - # ]) - # design_pre_json = sc.DNAOrigamiDesign(strands=[st_l, st_r, scaf], grid=sc.square) - # - # with self.assertRaises(sc.IllegalDesignError): - # design_pre_json.to_json() - - def test_domains_not_none_in_Strand_constructor(self) -> None: - with self.assertRaises(sc.IllegalDesignError): - sc.Strand(domains=None) # type: ignore - with self.assertRaises(sc.IllegalDesignError): - sc.Strand(domains=[None]) # type: ignore - - def test_strands_not_specified_in_Design_constructor(self) -> None: - design = sc.Design(helices=[]) - self.assertEqual(0, len(design.helices)) - self.assertEqual(0, len(design.strands)) - - def test_helices_not_specified_in_Design_constructor(self) -> None: - design = sc.Design(strands=[]) - self.assertEqual(0, len(design.helices)) - self.assertEqual(0, len(design.strands)) - - def test_strands_and_helices_not_specified_in_Design_constructor(self) -> None: - design = sc.Design() - self.assertEqual(0, len(design.helices)) - self.assertEqual(0, len(design.strands)) - - def test_consecutive_domains_loopout(self) -> None: - ss1 = sc.Domain(0, True, 0, 3) - ss2 = sc.Loopout(4) - ss3 = sc.Loopout(4) - with self.assertRaises(sc.IllegalDesignError): - sc.Strand([ss1, ss2, ss3]) - - # XXX: we used to allow Strands to violate the loopout rules and caught it only at the design level - # now the Strand constructor checks, so that means we can't set up a bad Strand for the Design check - # strand = sc.Strand([ss1, ss2]) - # strand.domains.append(ss3) - # helices = [sc.Helix(max_offset=10)] - # with self.assertRaises(sc.IllegalDesignError): - # sc.Design(helices=helices, strands=[strand], grid=sc.square) - - def test_singleton_loopout(self) -> None: - loopout = sc.Loopout(4) - with self.assertRaises(sc.StrandError): - sc.Strand([loopout]) - - # XXX: we used to allow Strands to violate the loopout rules and caught it only at the design level - # now the Strand constructor checks, so that means we can't set up a bad Strand for the Design check - # strand = sc.Strand([]) - # strand.domains.append(loopout) - # helices = [sc.Helix(max_offset=10)] - # with self.assertRaises(sc.StrandError): - # sc.Design(helices=helices, strands=[strand], grid=sc.square) - - def test_strand_offset_beyond_maxbases(self) -> None: - helices = [sc.Helix(max_offset=10)] - ss1 = sc.Domain(0, True, 0, 20) - strands = [sc.Strand([ss1])] - with self.assertRaises(sc.StrandError): - sc.Design(helices=helices, strands=strands) - - def test_assign_dna__conflicting_sequences_directly_assigned(self) -> None: - ss_right = sc.Domain(0, True, 0, 5) - ss_left = sc.Domain(0, False, 0, 5) - strand_right = sc.Strand([ss_right]) - strand_left = sc.Strand([ss_left]) - design = sc.Design(strands=[strand_left, strand_right]) - design.assign_dna(strand_right, 'ACGTT') - with self.assertRaises(sc.IllegalDesignError): - design.assign_dna(strand_right, 'TTTTT') - - def test_assign_dna__conflicting_sequences_indirectly_assigned(self) -> None: - ss_right = sc.Domain(0, True, 0, 5) - ss_left = sc.Domain(0, False, 0, 5) - strand_right = sc.Strand([ss_right]) - strand_left = sc.Strand([ss_left]) - design = sc.Design(strands=[strand_left, strand_right]) - design.assign_dna(strand_right, 'ACGTT') - with self.assertRaises(sc.IllegalDesignError): - design.assign_dna(strand_left, 'GGGGG') - - def test_overlapping_caught_in_strange_counterexample(self) -> None: - # found this counterexample as a simplified version of something caught in practice - s1_left_ss0 = sc.Domain(0, False, 0, 5) - s1_ss1 = sc.Domain(0, True, 0, 15) - s1_right_ss0 = sc.Domain(0, False, 5, 15) - s1 = sc.Strand([s1_left_ss0, s1_ss1, s1_right_ss0]) - - s2_ss1 = sc.Domain(0, True, 10, 20) - s2_ss0 = sc.Domain(0, False, 10, 20) - s2 = sc.Strand([s2_ss1, s2_ss0]) - - strands = [s1, s2] - - with self.assertRaises(sc.IllegalDesignError): - sc.Design(strands=strands, grid=sc.square) - - def test_major_tick_outside_range(self) -> None: - with self.assertRaises(sc.IllegalDesignError): - sc.Helix(max_offset=9, major_ticks=[2, 5, 10]) - - def test_major_tick_just_inside_range(self) -> None: - sc.Helix(max_offset=9, major_ticks=[0, 5, 9]) - - def test_two_illegally_overlapping_strands(self) -> None: - ss_bot = sc.Domain(helix=0, forward=False, start=0, end=9) - ss_top = sc.Domain(helix=0, forward=False, start=0, end=9) - strand_bot = sc.Strand(domains=[ss_bot]) - strand_top = sc.Strand(domains=[ss_top]) - strands = [strand_bot, strand_top] - with self.assertRaises(sc.IllegalDesignError): - sc.Design(grid=sc.square, strands=strands) - - def test_two_nonconsecutive_illegally_overlapping_strands(self) -> None: - ss_top1 = sc.Domain(helix=0, forward=False, start=0, end=5) - ss_bot = sc.Domain(helix=0, forward=True, start=2, end=9) - ss_top2 = sc.Domain(helix=0, forward=False, start=4, end=8) - strand_bot = sc.Strand(domains=[ss_bot]) - strand_top1 = sc.Strand(domains=[ss_top1]) - strand_top2 = sc.Strand(domains=[ss_top2]) - strands = [strand_bot, strand_top1, strand_top2] - with self.assertRaises(sc.IllegalDesignError): - sc.Design(grid=sc.square, strands=strands) - - def test_four_legally_leapfrogging_strands(self) -> None: - ss_top1 = sc.Domain(helix=0, forward=False, start=0, end=20) - ss_bot1 = sc.Domain(helix=0, forward=True, start=10, end=30) - ss_top2 = sc.Domain(helix=0, forward=False, start=20, end=40) - ss_bot2 = sc.Domain(helix=0, forward=True, start=30, end=50) - strand_bot1 = sc.Strand(domains=[ss_bot1]) - strand_bot2 = sc.Strand(domains=[ss_bot2]) - strand_top1 = sc.Strand(domains=[ss_top1]) - strand_top2 = sc.Strand(domains=[ss_top2]) - strands = [strand_bot1, strand_bot2, strand_top1, strand_top2] - sc.Design(grid=sc.square, strands=strands) - - def test_strand_references_nonexistent_helix(self) -> None: - h1 = sc.Helix(max_offset=9) - h2 = sc.Helix(max_offset=9) - ss_bot = sc.Domain(helix=2, forward=False, start=0, end=9) - ss_top = sc.Domain(helix=3, forward=False, start=0, end=9) - strand_bot = sc.Strand(domains=[ss_bot]) - strand_top = sc.Strand(domains=[ss_top]) - strands = [strand_bot, strand_top] - with self.assertRaises(sc.IllegalDesignError): - sc.Design(grid=sc.square, helices=[h1, h2], strands=strands) - - -class TestInsertRemoveDomains(unittest.TestCase): - - def setUp(self) -> None: - helices = [sc.Helix(max_offset=100) for _ in range(4)] - self.design = sc.Design(helices=helices, strands=[]) - self.design.draw_strand(0, 0).to(3).cross(1).to(0).cross(2).to(3).with_sequence('ACA TCT GTG') - self.strand = self.design.strands[0] - - def test_3_helix_before_design(self) -> None: - expected_strand_before = sc.Strand([ - sc.Domain(0, True, 0, 3), - sc.Domain(1, False, 0, 3), - sc.Domain(2, True, 0, 3), - ], dna_sequence='ACA TCT GTG'.replace(' ', '')) - self.assertEqual(expected_strand_before, self.strand) - - def test_insert_domain_with_sequence(self) -> None: - helices = [sc.Helix(max_offset=100) for _ in range(4)] - design = sc.Design(helices=helices, strands=[]) - design.draw_strand(0, 0).to(3).cross(1).to(0).cross(3).to(3).with_sequence('ACA TCT GTG') - strand = design.strands[0] - - expected_strand_before = sc.Strand([ - sc.Domain(0, True, 0, 3), - sc.Domain(1, False, 0, 3), - sc.Domain(3, True, 0, 3), - ], dna_sequence='ACA TCT GTG'.replace(' ', '')) - self.assertEqual(expected_strand_before, design.strands[0]) - - domain = sc.Domain(2, True, 0, 3) - design.insert_domain(strand, 2, domain) - expected_strand = sc.Strand([ - sc.Domain(0, True, 0, 3), - sc.Domain(1, False, 0, 3), - sc.Domain(2, True, 0, 3), - sc.Domain(3, True, 0, 3), - ], dna_sequence='ACA TCT ??? GTG'.replace(' ', '')) - self.assertEqual(expected_strand, design.strands[0]) - - def test_append_domain_with_sequence(self) -> None: - domain = sc.Domain(3, False, 0, 3) - self.design.append_domain(self.strand, domain) - expected_strand = sc.Strand([ - sc.Domain(0, True, 0, 3), - sc.Domain(1, False, 0, 3), - sc.Domain(2, True, 0, 3), - sc.Domain(3, False, 0, 3), - ], dna_sequence='ACA TCT GTG ???'.replace(' ', '')) - self.assertEqual(expected_strand, self.strand) - - def test_remove_first_domain_with_sequence(self) -> None: - self.design.remove_domain(self.strand, self.strand.domains[0]) - expected_strand = sc.Strand([ - sc.Domain(1, False, 0, 3), - sc.Domain(2, True, 0, 3), - ], dna_sequence=' TCT GTG'.replace(' ', '')) - self.assertEqual(expected_strand, self.strand) - - def test_remove_middle_domain_with_sequence(self) -> None: - self.design.remove_domain(self.strand, self.strand.domains[1]) - expected_strand = sc.Strand([ - sc.Domain(0, True, 0, 3), - sc.Domain(2, True, 0, 3), - ], dna_sequence='ACA GTG'.replace(' ', '')) - self.assertEqual(expected_strand, self.strand) - - def test_remove_last_domain_with_sequence(self) -> None: - self.design.remove_domain(self.strand, self.strand.domains[2]) - expected_strand = sc.Strand([ - sc.Domain(0, True, 0, 3), - sc.Domain(1, False, 0, 3), - ], dna_sequence='ACA TCT'.replace(' ', '')) - self.assertEqual(expected_strand, self.strand) - - -class TestLabels(unittest.TestCase): - - def setUp(self) -> None: - helices = [sc.Helix(max_offset=100) for _ in range(10)] - self.design = sc.Design(helices=helices, strands=[], grid=sc.square) - - def test_with_label__str(self) -> None: - label = 'abc' - self.design.draw_strand(0, 0).to(5).cross(1).to(0).with_label(label) - actual_strand = self.design.strands[0] - expected_strand = sc.Strand(domains=[ - sc.Domain(0, True, 0, 5), - sc.Domain(0, False, 0, 5), - ], label=label) - - self.assertEqual(expected_strand.label, actual_strand.label) - - def test_with_label__dict(self) -> None: - label = {'name': 'abc', 'type': 3} - self.design.draw_strand(0, 0).to(5).cross(1).to(0).with_label(label) - actual_strand = self.design.strands[0] - expected_strand = sc.Strand(domains=[ - sc.Domain(0, True, 0, 5), - sc.Domain(0, False, 0, 5), - ], label=label) - - self.assertDictEqual(expected_strand.label, actual_strand.label) - - def test_with_domain_label(self) -> None: - label0: Union[str, Dict[str, Any]] = 'abc' - label1: Union[str, Dict[str, Any]] = {'name': 'abc', 'type': 3} - self.design.draw_strand(0, 0).to(5).with_domain_label(label0).cross(1).to(0).with_domain_label(label1) - actual_strand = self.design.strands[0] - expected_strand = sc.Strand(domains=[ - sc.Domain(0, True, 0, 5, label=label0), - sc.Domain(0, False, 0, 5, label=label1), - ]) - - self.assertEqual(expected_strand.domains[0].label, actual_strand.domains[0].label) - self.assertDictEqual(expected_strand.domains[1].label, actual_strand.domains[1].label) - - def test_with_domain_label__and__with_label(self) -> None: - strand_label = 'xyz' - label0: Union[str, Dict[str, Any]] = 'abc' - label1: Union[str, Dict[str, Any]] = {'name': 'abc', 'type': 3} - self.design.draw_strand(0, 0).to(5).with_domain_label(label0).cross(1).to(0).with_domain_label(label1) \ - .with_label(strand_label) - actual_strand = self.design.strands[0] - expected_strand = sc.Strand(domains=[ - sc.Domain(0, True, 0, 5, label=label0), - sc.Domain(0, False, 0, 5, label=label1), - ], label=strand_label) - - self.assertEqual(expected_strand.label, actual_strand.label) - self.assertEqual(expected_strand.domains[0].label, actual_strand.domains[0].label) - self.assertDictEqual(expected_strand.domains[1].label, actual_strand.domains[1].label) - - -def set_colors_black(*strands) -> None: - for strand in strands: - strand.set_color(sc.Color(r=0, g=0, b=0)) - - -class TestCircularStrandsLegalMods(unittest.TestCase): - ''' - Tests that circular strands cannot have 5' or 3' mods. - ''' - - def setUp(self) -> None: - helices = [sc.Helix(max_offset=10) for _ in range(2)] - self.design = sc.Design(helices=helices, strands=[]) - self.design.draw_strand(0, 0).move(10).cross(1).move(-10) - self.strand = self.design.strands[0] - r''' - 0 [--------\ - | - 1 <--------/ - ''' - - def test_can_add_internal_mod_to_circular_strand(self) -> None: - self.strand.set_circular() - self.assertTrue(self.strand.circular) - self.strand.set_modification_internal(2, mod.biotin_int) - self.assertEqual(1, len(self.strand.modifications_int)) - - def test_cannot_make_strand_circular_if_5p_mod(self) -> None: - self.strand.set_modification_5p(mod.biotin_5p) - with self.assertRaises(sc.StrandError): - self.strand.set_circular(True) - - def test_cannot_make_strand_circular_if_3p_mod(self) -> None: - self.strand.set_modification_3p(mod.biotin_3p) - with self.assertRaises(sc.StrandError): - self.strand.set_circular(True) - - def test_add_5p_mod_to_circular_strand(self) -> None: - self.strand.set_circular(True) - with self.assertRaises(sc.StrandError): - self.strand.set_modification_5p(mod.biotin_5p) - - def test_add_3p_mod_to_circular_strand(self) -> None: - self.strand.set_circular(True) - with self.assertRaises(sc.StrandError): - self.strand.set_modification_3p(mod.biotin_3p) - - -class TestCircularStrandEdits(unittest.TestCase): - ''' - Tests that circular strand edits (nicking, ligating, adding crossovers/loopouts) - works properly with circular strands. - ''' - - def setUp(self) -> None: - helices = [sc.Helix(max_offset=50) for _ in range(3)] - self.design = sc.Design(helices=helices, strands=[]) - self.design.draw_strand(0, 0).move(10).cross(1).move(-10) - self.design.draw_strand(0, 15).move(5).cross(1).move(-10).cross(0).move(5) - self.design.draw_strand(0, 20).move(10) - self.design.draw_strand(1, 30).move(-10) - self.design.draw_strand(0, 30).move(10).loopout(1, 5).move(-10).cross(2).move(10).as_circular() - self.design.draw_strand(0, 40).move(10).cross(1).move(-10).as_circular() - self.num_strands = len(self.design.strands) - r''' - 0 10 20 30 40 - strand 0 strand 1 strand 2 strand 4 strand 5 - 0 [--------\ /--->[---\ [--------> [--------\ /--------\ - | | | ) | | - 1 <--------/ \--------/ <--------] /--------/ \--------/ - strand 3 | - 2 \-------->cross to 5' end here - ''' - - def test_add_crossover_from_linear_strand_to_itself_makes_it_circular(self) -> None: - # add crossover to strand 0 - r''' - 0 - strand 0 - 0 /--------\ - | | - 1 \--------/ - ''' - self.assertEqual(2, len(self.design.strands[0].domains)) - self.assertFalse(self.design.strands[0].circular) - self.design.add_half_crossover(0, 1, 0, True) - self.assertTrue(self.design.strands[0].circular) - self.assertEqual(self.num_strands, len(self.design.strands)) - self.assertEqual(2, len(self.design.strands[0].domains)) - - def test_add_nick_to_2_domain_circular_strand_makes_it_linear_nick_first_domain(self) -> None: - r''' - 40 - strand 5 - 0 /--->[---\ - | | - 1 \--------/ - ''' - self.assertTrue(self.design.strands[5].circular) - - # nick strand 5 - self.design.add_nick(0, 45, True) - self.assertFalse(self.design.strands[5].circular) - self.assertEqual(self.num_strands, len(self.design.strands)) - - strand = self.design.strands[5] - self.assertEqual(3, len(strand.domains)) - self.assertEqual(3, len(strand.bound_domains())) - - d0, d1, d2 = strand.bound_domains() - self.assertEqual(0, d0.helix) - self.assertEqual(True, d0.forward) - self.assertEqual(45, d0.start) - self.assertEqual(50, d0.end) - - self.assertEqual(1, d1.helix) - self.assertEqual(False, d1.forward) - self.assertEqual(40, d1.start) - self.assertEqual(50, d1.end) - - self.assertEqual(0, d2.helix) - self.assertEqual(True, d2.forward) - self.assertEqual(40, d2.start) - self.assertEqual(45, d2.end) - - for domain in strand.domains: - self.assertIs(strand, domain.strand()) - - def test_add_nick_to_2_domain_circular_strand_makes_it_linear_nick_second_domain(self) -> None: - # nick strand 5 - r''' - 40 - strand 5 - 0 /--------\ - | | - 1 \---]<---/ - ''' - self.design.add_nick(1, 45, False) - strand = self.design.strands[5] - self.assertFalse(strand.circular) - self.assertEqual(self.num_strands, len(self.design.strands)) - - self.assertEqual(3, len(strand.domains)) - self.assertEqual(3, len(strand.bound_domains())) - - d0, d1, d2 = strand.bound_domains() - self.assertEqual(1, d0.helix) - self.assertEqual(False, d0.forward) - self.assertEqual(40, d0.start) - self.assertEqual(45, d0.end) - - self.assertEqual(0, d1.helix) - self.assertEqual(True, d1.forward) - self.assertEqual(40, d1.start) - self.assertEqual(50, d1.end) - - self.assertEqual(1, d2.helix) - self.assertEqual(False, d2.forward) - self.assertEqual(45, d2.start) - self.assertEqual(50, d2.end) - - for domain in strand.domains: - self.assertIs(strand, domain.strand()) - - def test_add_nick_to_3_domain_circular_strand_makes_it_linear_nick_first_domain(self) -> None: - # nick strand 4 - r''' - 30 40 - strand 4 - 0 [--->[---\ - ) loopout length 5 - 1 /--------/ - | - 2 \-------->cross to 5' end here - ''' - self.design.add_nick(0, 35, True) - strand = self.design.strands[4] - self.assertFalse(strand.circular) - self.assertEqual(self.num_strands, len(self.design.strands)) - - self.assertEqual(5, len(strand.domains)) - self.assertEqual(4, len(strand.bound_domains())) - - d0, loopout, d1, d2, d3 = strand.domains - self.assertIsInstance(loopout, sc.Loopout) - - self.assertEqual(0, d0.helix) - self.assertEqual(True, d0.forward) - self.assertEqual(35, d0.start) - self.assertEqual(40, d0.end) - - self.assertEqual(1, d1.helix) - self.assertEqual(False, d1.forward) - self.assertEqual(30, d1.start) - self.assertEqual(40, d1.end) - - self.assertEqual(2, d2.helix) - self.assertEqual(True, d2.forward) - self.assertEqual(30, d2.start) - self.assertEqual(40, d2.end) - - self.assertEqual(0, d3.helix) - self.assertEqual(True, d3.forward) - self.assertEqual(30, d3.start) - self.assertEqual(35, d3.end) - - for domain in strand.domains: - self.assertIs(strand, domain.strand()) - - def test_add_nick_to_3_domain_circular_strand_makes_it_linear_nick_middle_domain(self) -> None: - # nick strand 4 - r''' - 30 40 - strand 4 - 0 [--------\ - ) loopout length 5 - 1 /---]<---/ - | - 2 \-------->cross to 5' end here - ''' - self.design.add_nick(1, 35, False) - strand = self.design.strands[4] - self.assertFalse(strand.circular) - self.assertEqual(self.num_strands, len(self.design.strands)) - - self.assertEqual(5, len(strand.domains)) - self.assertEqual(4, len(strand.bound_domains())) - - d0, d1, d2, loopout, d3 = strand.domains - self.assertIsInstance(loopout, sc.Loopout) - - self.assertEqual(1, d0.helix) - self.assertEqual(False, d0.forward) - self.assertEqual(30, d0.start) - self.assertEqual(35, d0.end) - - self.assertEqual(2, d1.helix) - self.assertEqual(True, d1.forward) - self.assertEqual(30, d1.start) - self.assertEqual(40, d1.end) - - self.assertEqual(0, d2.helix) - self.assertEqual(True, d2.forward) - self.assertEqual(30, d2.start) - self.assertEqual(40, d2.end) - - self.assertEqual(1, d3.helix) - self.assertEqual(False, d3.forward) - self.assertEqual(35, d3.start) - self.assertEqual(40, d3.end) - - for domain in strand.domains: - self.assertIs(strand, domain.strand()) - - def test_add_nick_to_3_domain_circular_strand_makes_it_linear_nick_last_domain(self) -> None: - # nick strand 4 - r''' - 30 40 - strand 4 - 0 [--------\ - ) loopout length 5 - 1 /--------/ - | - 2 \--->[--->cross to 5' end here - ''' - self.design.add_nick(2, 35, True) - strand = self.design.strands[4] - self.assertFalse(strand.circular) - self.assertEqual(self.num_strands, len(self.design.strands)) - - self.assertEqual(5, len(strand.domains)) - self.assertEqual(4, len(strand.bound_domains())) - - d0, d1, loopout, d2, d3 = strand.domains - self.assertIsInstance(loopout, sc.Loopout) - - self.assertEqual(2, d0.helix) - self.assertEqual(True, d0.forward) - self.assertEqual(35, d0.start) - self.assertEqual(40, d0.end) - - self.assertEqual(0, d1.helix) - self.assertEqual(True, d1.forward) - self.assertEqual(30, d1.start) - self.assertEqual(40, d1.end) - - self.assertEqual(1, d2.helix) - self.assertEqual(False, d2.forward) - self.assertEqual(30, d2.start) - self.assertEqual(40, d2.end) - - self.assertEqual(2, d3.helix) - self.assertEqual(True, d3.forward) - self.assertEqual(30, d3.start) - self.assertEqual(35, d3.end) - - for domain in strand.domains: - self.assertIs(strand, domain.strand()) - - def test_ligate_linear_strand_to_itself_makes_it_circular(self) -> None: - self.assertFalse(self.design.strands[1].circular) - self.assertEqual(3, len(self.design.strands[1].domains)) - - self.design.ligate(0, 15, True) - - strand = self.design.strands[1] - self.assertEqual(self.num_strands, len(self.design.strands)) - self.assertTrue(strand.circular) - self.assertEqual(2, len(strand.domains)) - - for domain in strand.domains: - self.assertIs(strand, domain.strand()) - - # TODO: add functionality for removing crossovers and loopouts, and test that here - - -class TestAddStrand(unittest.TestCase): - - def test_add_strand__with_loopout(self) -> None: - helices = [sc.Helix(max_offset=10), sc.Helix(max_offset=10)] - design = sc.Design(helices=helices, strands=[]) - - ss1 = sc.Domain(0, True, 0, 10) - loop = sc.Loopout(4) - ss2 = sc.Domain(1, False, 0, 10) - strand = sc.Strand([ss1, loop, ss2]) - - design.add_strand(strand) - - self.assertEqual(1, len(design.strands)) - self.assertEqual(strand, design.strands[0]) - self.assertEqual(ss1, design.domain_at(0, 0, True)) - self.assertEqual(ss2, design.domain_at(1, 0, False)) - - def test_add_strand__illegal_overlapping_domains(self) -> None: - helices = [sc.Helix(max_offset=50), sc.Helix(max_offset=50)] - design = sc.Design(helices=helices, strands=[], grid=sc.square) - with self.assertRaises(sc.StrandError): - strand = sc.Strand([ - sc.Domain(0, False, 40, 48), - sc.Domain(0, False, 32, 48, deletions=[44]), - sc.Domain(1, True, 32, 40), - ]) - design.add_strand(strand) - - -class TestAssignDNA(unittest.TestCase): - - def test_assign_dna__hairpin(self) -> None: - """ - 01234 - AAACC # helix 0 going forward - \ - T # loopout - G # loopout - C # loopout - A # loopout - C # loopout - / - TTTGG # helix 0 going reverse - """ - ss_f = sc.Domain(helix=0, forward=True, start=0, end=5) - loop = sc.Loopout(length=5) - ss_r = sc.Domain(helix=0, forward=False, start=0, end=5) - strand_forward = sc.Strand([ss_f, loop, ss_r]) - design = sc.Design(strands=[strand_forward], grid=sc.square) - design.assign_dna(strand_forward, 'AAACC TGCAC') - self.assertEqual('AAACC TGCAC GGTTT'.replace(' ', ''), strand_forward.dna_sequence) - - def test_assign_dna__from_strand_with_loopout(self) -> None: - """ - 01234 - <-TTTGG-] - [-AAACC- # helix 0 - \ - T # loopout - G # loopout - C # loopout - A # loopout - C # loopout - / - <-GCTTA- # helix 1 - [-CGAAT-> - """ - ss_f = sc.Domain(helix=0, forward=True, start=0, end=5) - loop = sc.Loopout(length=5) - ss_r = sc.Domain(helix=1, forward=False, start=0, end=5) - strand_multi = sc.Strand([ss_f, loop, ss_r]) - - ss_single0 = sc.Domain(helix=0, forward=False, start=0, end=5) - strand_single0 = sc.Strand([ss_single0]) - - ss_single1 = sc.Domain(helix=1, forward=True, start=0, end=5) - strand_single1 = sc.Strand([ss_single1]) - - design = sc.Design(strands=[strand_multi, strand_single0, strand_single1], grid=sc.square) - - design.assign_dna(strand_multi, 'AAACC TGCAC ATTCG') - - self.assertEqual('AAACC TGCAC ATTCG'.replace(' ', ''), strand_multi.dna_sequence) - self.assertEqual('GGTTT'.replace(' ', ''), strand_single0.dna_sequence) - self.assertEqual('CGAAT'.replace(' ', ''), strand_single1.dna_sequence) - - def test_assign_dna__to_strand_with_loopout(self) -> None: - """ - 01234 - <-TTTGG-] - [-AAACC- # helix 0 - \ - ? # loopout - ? # loopout - ? # loopout - ? # loopout - ? # loopout - / - <-GCTTA- # helix 1 - [-CGAAT-> - """ - ss_f = sc.Domain(helix=0, forward=True, start=0, end=5) - loop = sc.Loopout(length=5) - ss_r = sc.Domain(helix=1, forward=False, start=0, end=5) - strand_multi = sc.Strand([ss_f, loop, ss_r]) - - ss_single0 = sc.Domain(helix=0, forward=False, start=0, end=5) - strand_single0 = sc.Strand([ss_single0]) - - ss_single1 = sc.Domain(helix=1, forward=True, start=0, end=5) - strand_single1 = sc.Strand([ss_single1]) - - design = sc.Design(strands=[strand_multi, strand_single0, strand_single1], grid=sc.square) - - design.assign_dna(strand_single0, 'GGTTT') - - self.assertEqual('AAACC ????? ?????'.replace(' ', ''), strand_multi.dna_sequence) - self.assertEqual('GGTTT'.replace(' ', ''), strand_single0.dna_sequence) - - design.assign_dna(strand_single1, 'CGAAT') - - self.assertEqual('AAACC ????? ATTCG'.replace(' ', ''), strand_multi.dna_sequence) - self.assertEqual('GGTTT'.replace(' ', ''), strand_single0.dna_sequence) - self.assertEqual('CGAAT'.replace(' ', ''), strand_single1.dna_sequence) - - def test_assign_dna__assign_from_strand_multi_other_single(self) -> None: - """ - 01234567 - <-TTTG----GACA-] - +-AAAC->[-CTGT-+ # helix 0 - | | - +-GCTT----AGTA-+ # helix 1 - """ - ss_f_left = sc.Domain(helix=0, forward=True, start=0, end=4) - ss_f_right = sc.Domain(helix=0, forward=True, start=4, end=8) - ss_h1 = sc.Domain(helix=1, forward=False, start=0, end=8) - strand_multi = sc.Strand([ss_f_right, ss_h1, ss_f_left]) - - ss_single = sc.Domain(helix=0, forward=False, start=0, end=8) - strand_single = sc.Strand([ss_single]) - - design = sc.Design(strands=[strand_multi, strand_single], grid=sc.square) - - design.assign_dna(strand_multi, 'CTGT ATGA TTCG AAAC') - - self.assertEqual('CTGT ATGA TTCG AAAC'.replace(' ', ''), strand_multi.dna_sequence) - self.assertEqual('ACAG GTTT'.replace(' ', ''), strand_single.dna_sequence) - - def test_assign_dna__assign_to_strand_multi_other_single(self) -> None: - """ - 01234567 - <-TTTG----GACA-] - +-AAAC->[-CTGT-+ # helix 0 - | | - +-????----????-+ # helix 1 - """ - ss_f_left = sc.Domain(helix=0, forward=True, start=0, end=4) - ss_f_right = sc.Domain(helix=0, forward=True, start=4, end=8) - ss_h1 = sc.Domain(helix=1, forward=False, start=0, end=8) - strand_multi = sc.Strand([ss_f_right, ss_h1, ss_f_left]) - - ss_single = sc.Domain(helix=0, forward=False, start=0, end=8) - strand_single = sc.Strand([ss_single]) - - design = sc.Design(strands=[strand_multi, strand_single], grid=sc.square) - - design.assign_dna(strand_single, 'ACAG GTTT') - - self.assertEqual('CTGT ???? ???? AAAC'.replace(' ', ''), strand_multi.dna_sequence) - self.assertEqual('ACAG GTTT'.replace(' ', ''), strand_single.dna_sequence) - - def test_assign_dna__other_strand_fully_defined_already(self) -> None: - """ - 01234567 - [------> - CAAAGTCG - GTTT - <--] - """ - ss_r = sc.Domain(helix=0, forward=True, start=0, end=8) - ss_l = sc.Domain(helix=0, forward=False, start=0, end=4) - strand_r = sc.Strand(domains=[ss_r]) - strand_l = sc.Strand(domains=[ss_l]) - design = sc.Design(grid=sc.square, strands=[strand_r, strand_l]) - design.assign_dna(strand_r, 'CAAAGTCG') - design.assign_dna(strand_l, 'TTTG') - # should not have an error by this point - - def test_assign_dna__other_strand_fully_defined_already_and_other_extends_beyond(self) -> None: - """ - 01234567 - [------> - CAAAGTCG - TTCA - <--] - """ - ss_r = sc.Domain(helix=0, forward=True, start=0, end=8) - ss_l = sc.Domain(helix=0, forward=False, start=2, end=6) - strand_r = sc.Strand(domains=[ss_r]) - strand_l = sc.Strand(domains=[ss_l]) - design = sc.Design(grid=sc.square, strands=[strand_r, strand_l]) - design.assign_dna(strand_r, 'CAAAGTCG') - design.assign_dna(strand_l, 'ACTT') - # should not have an error by this point - - def test_assign_dna__other_strand_fully_defined_already_and_self_extends_beyond(self) -> None: - """ - 01234567 - [------> - CAAAGTCG - TTCA - <--] - """ - ss_r = sc.Domain(helix=0, forward=True, start=0, end=8) - ss_l = sc.Domain(helix=0, forward=False, start=2, end=6) - strand_r = sc.Strand(domains=[ss_r]) - strand_l = sc.Strand(domains=[ss_l]) - design = sc.Design(grid=sc.square, strands=[strand_r, strand_l]) - design.assign_dna(strand_l, 'ACTT') - design.assign_dna(strand_r, 'CAAAGTCG') - # should not have an error by this point - - def test_assign_dna__two_equal_length_strands_on_one_helix(self) -> None: - """ - 01234 - <---] - CAAAA - GTTTT - [---> - """ - ss_r = sc.Domain(helix=0, forward=True, start=0, end=5) - ss_l = sc.Domain(helix=0, forward=False, start=0, end=5) - strand_r = sc.Strand(domains=[ss_r]) - strand_l = sc.Strand(domains=[ss_l]) - design = sc.Design(grid=sc.square, strands=[strand_r, strand_l]) - design.assign_dna(strand_l, 'AAAAC') - self.assertEqual('GTTTT', strand_r.dna_sequence) - - def test_assign_dna__assign_seq_with_wildcards(self) -> None: - """ - 01234 - <---] - C??AA - G??TT - [---> - """ - ss_bot = sc.Domain(helix=0, forward=True, start=0, end=5) - ss_top = sc.Domain(helix=0, forward=False, start=0, end=5) - strand_bot = sc.Strand(domains=[ss_bot]) - strand_top = sc.Strand(domains=[ss_top]) - strands = [strand_bot, strand_top] - design = sc.Design(grid=sc.square, strands=strands) - design.assign_dna(strand_top, 'AA??C') - self.assertEqual('G??TT', strand_bot.dna_sequence) - - def test_assign_dna__one_strand_assigned_by_complement_from_two_other_strands(self) -> None: - """ - 0123 4567 - <-AAAC-] <-GGGA-] - [-TTTG-----CCCT-> - """ - ss_top_left = sc.Domain(0, False, 0, 4) - ss_top_right = sc.Domain(0, False, 4, 8) - ss_bot = sc.Domain(0, True, 0, 8) - st_top_left = sc.Strand([ss_top_left]) - st_top_right = sc.Strand([ss_top_right]) - st_bot = sc.Strand([ss_bot]) - design = sc.Design(strands=[st_bot, st_top_left, st_top_right], grid=sc.square) - design.assign_dna(st_top_left, 'CAAA') - self.assertEqual('TTTG????', st_bot.dna_sequence) - design.assign_dna(st_top_right, 'AGGG') - self.assertEqual('TTTGCCCT', st_bot.dna_sequence) - - def test_assign_dna__adapter_assigned_from_scaffold_and_tiles(self) -> None: - """ - XXX: it appears the behavior this tests (which the other tests miss) is assigning DNA to - tile0 first, then to tile1, and adap is connected to each of them on different helices. - This means that when tile1 is assigned, we need to ensure when assigning to adap that we - keep the old information and don't discard it by simply padding the shorter portion of it on - helix 1 with ?'s, but remember the old DNA sequence. - - 01 2345 6789 01 - adap [-TTTC-----CATT-------+ - scaf <-GT-AAAG-+ <-GTAA--AA-] | - | | - [-AA-TTTG-+ [-TGCC--GG-> | - <-AAAC-----ACGG-------+ - """ - scaf0_ss = sc.Domain(0, False, 0, 6) - scaf1_ss = sc.Domain(1, True, 0, 6) - tile1_ss = sc.Domain(1, True, 6, 12) - tile0_ss = sc.Domain(0, False, 6, 12) - adap0_ss = sc.Domain(0, True, 2, 10) - adap1_ss = sc.Domain(1, False, 2, 10) - scaf = sc.Strand([scaf1_ss, scaf0_ss]) - adap = sc.Strand([adap0_ss, adap1_ss]) - tile0 = sc.Strand([tile0_ss]) - tile1 = sc.Strand([tile1_ss]) - - design = sc.Design(strands=[scaf, adap, tile0, tile1]) - - design.assign_dna(tile0, 'AA AATG') - self.assertEqual('???? CATT ???? ????'.replace(' ', ''), adap.dna_sequence) - - design.assign_dna(tile1, 'TGCC GG') - self.assertEqual('???? CATT GGCA ????'.replace(' ', ''), adap.dna_sequence) - - design.assign_dna(scaf, 'AA TTTG GAAA TG') - self.assertEqual('TTTC CATT GGCA CAAA'.replace(' ', ''), adap.dna_sequence) - - def test_assign_dna__adapter_assigned_from_scaffold_and_tiles_with_deletions(self) -> None: - """ - XXX: it appears the behavior this tests (which the other tests miss) is assigning DNA to - tile0 first, then to tile1, and adap is connected to each of them on different helices. - This means that when tile1 is assigned, we need to ensure when assigning to adap that we - keep the old information and don't discard it by simply padding the shorter portion of it on - helix 1 with ?'s, but remember the old DNA sequence. - - 01 2345 6789 01 - X X deletions - adap [-T TC-----CA T-------+ - scaf <-GT-A AG-+ <-GT A--AA-] | - | | - [-AA-TTTG-+ [-TG C--GG-> | - <-AAAC-----AC G-------+ - X deletions - """ - scaf0_ss = sc.Domain(0, False, 0, 6) - scaf1_ss = sc.Domain(1, True, 0, 6) - tile1_ss = sc.Domain(1, True, 6, 12) - tile0_ss = sc.Domain(0, False, 6, 12) - adap0_ss = sc.Domain(0, True, 2, 10) - adap1_ss = sc.Domain(1, False, 2, 10) - scaf = sc.Strand([scaf1_ss, scaf0_ss]) - adap = sc.Strand([adap0_ss, adap1_ss]) - tile0 = sc.Strand([tile0_ss]) - tile1 = sc.Strand([tile1_ss]) - - design = sc.Design(strands=[scaf, adap, tile0, tile1]) - design.add_deletion(0, 3) - design.add_deletion(0, 8) - design.add_deletion(1, 8) - - design.assign_dna(tile0, 'AA ATG') - self.assertEqual('??? CAT ??? ????'.replace(' ', ''), adap.dna_sequence) - - design.assign_dna(tile1, 'TGC GG') - self.assertEqual('??? CAT GCA ????'.replace(' ', ''), adap.dna_sequence) - - design.assign_dna(scaf, 'AA TTTG GAA TG') - self.assertEqual('TTC CAT GCA CAAA'.replace(' ', ''), adap.dna_sequence) - - def test_assign_dna__adapter_assigned_from_scaffold_and_tiles_with_insertions(self) -> None: - """ - XXX: it appears the behavior this tests (which the other tests miss) is assigning DNA to - tile0 first, then to tile1, and adap is connected to each of them on different helices. - This means that when tile1 is assigned, we need to ensure when assigning to adap that we - keep the old information and don't discard it by simply padding the shorter portion of it on - helix 1 with ?'s, but remember the old DNA sequence. - - 01 2345 678I9 01 - I insertions - adap [-TTTC-----CATTT-------+ - scaf <-GT-AAAG-+ <-GTAAA--AA-] | - | | - [-AA-TTTG-+ [-TGCCC--GG-> | - <-AAAC-----ACGGG-------+ - I insertions - """ - scaf0_ss = sc.Domain(0, False, 0, 6) - scaf1_ss = sc.Domain(1, True, 0, 6) - tile1_ss = sc.Domain(1, True, 6, 12) - tile0_ss = sc.Domain(0, False, 6, 12) - adap0_ss = sc.Domain(0, True, 2, 10) - adap1_ss = sc.Domain(1, False, 2, 10) - scaf = sc.Strand([scaf1_ss, scaf0_ss]) - adap = sc.Strand([adap0_ss, adap1_ss]) - tile0 = sc.Strand([tile0_ss]) - tile1 = sc.Strand([tile1_ss]) - - design = sc.Design(strands=[scaf, adap, tile0, tile1]) - design.add_insertion(0, 8, 1) - design.add_insertion(1, 8, 1) - - design.assign_dna(tile0, 'AA AAATG') - self.assertEqual('???? CATTT ????? ????'.replace(' ', ''), adap.dna_sequence) - - design.assign_dna(tile1, 'TGCCC GG') - self.assertEqual('???? CATTT GGGCA ????'.replace(' ', ''), adap.dna_sequence) - - design.assign_dna(scaf, 'AA TTTG GAAA TG') - self.assertEqual('TTTC CATTT GGGCA CAAA'.replace(' ', ''), adap.dna_sequence) - - def test_assign_dna__dna_sequence_shorter_than_complementary_strand_right_strand_longer(self) -> None: - """ - <---] - CAAAA - GTTTT????? - [--------> - """ - ss_long = sc.Domain(helix=0, forward=True, start=0, end=10) - ss_short = sc.Domain(helix=0, forward=False, start=0, end=5) - strand_long = sc.Strand(domains=[ss_long]) - strand_short = sc.Strand(domains=[ss_short]) - strands = [strand_long, strand_short] - design = sc.Design(grid=sc.square, strands=strands) - design.assign_dna(strand_short, 'AAAAC') - self.assertEqual('GTTTT?????', strand_long.dna_sequence) - - def test_assign_dna__dna_sequence_shorter_than_complementary_strand_left_strand_longer(self) -> None: - """ - [---> - AAAAC - TTTTG????? - <--------] - """ - ss_long = sc.Domain(helix=0, forward=False, start=0, end=10) - ss_short = sc.Domain(helix=0, forward=True, start=0, end=5) - strand_long = sc.Strand(domains=[ss_long]) - strand_short = sc.Strand(domains=[ss_short]) - strands = [strand_long, strand_short] - design = sc.Design(grid=sc.square, strands=strands) - design.assign_dna(strand_short, 'AAAAC') - self.assertEqual('?????GTTTT', strand_long.dna_sequence) - - def test_assign_dna__dna_sequence_with_uncomplemented_domain_on_different_helix(self) -> None: - """ - <---] - CAAAA - GTTTT????? - [--------+ - | - <-+ - ??? - """ - ss_long = sc.Domain(helix=0, forward=True, start=0, end=10) - ss_long_h1 = sc.Domain(helix=0, forward=False, start=7, end=10) - ss_short = sc.Domain(helix=0, forward=False, start=0, end=5) - strand_long = sc.Strand(domains=[ss_long, ss_long_h1]) - strand_short = sc.Strand(domains=[ss_short]) - strands = [strand_long, strand_short] - design = sc.Design(grid=sc.square, strands=strands) - design.assign_dna(strand_short, 'AAAAC') - self.assertEqual('GTTTT????????', strand_long.dna_sequence) - - def test_assign_dna__dna_sequence_with_uncomplemented_domain_on_different_helix_wildcards_both_ends( - self) -> None: - """ - <---] - CAAAA - ?????GTTTT - [--------+ - | - <-+ - ??? - """ - ss_long_h0 = sc.Domain(helix=0, forward=True, start=0, end=10) - ss_long_h1 = sc.Domain(helix=1, forward=False, start=7, end=10) - ss_short_h0 = sc.Domain(helix=0, forward=False, start=5, end=10) - strand_long = sc.Strand(domains=[ss_long_h0, ss_long_h1]) - strand_short = sc.Strand(domains=[ss_short_h0]) - strands = [strand_long, strand_short] - design = sc.Design(grid=sc.square, strands=strands) - design.assign_dna(strand_short, 'AAAAC') - self.assertEqual('?????GTTTT???', strand_long.dna_sequence) - - def test_assign_dna__one_helix_with_one_bottom_strand_and_three_top_strands(self) -> None: - """ - 012 345 678 - -TTT> -GGG> -CCC> - None: - """ - 012 345 678 901 - M13 [-ACC----TAA---GAA----AAC---+ - +-TGG-]<-ATT-+ CTT----TTG-+ | - | | | | | - +-GAT----TTC-+ ATG->[-AGT-+ | - <-CTA----AAG---TAC----TCA---+ - """ - scaf0_ss = sc.Domain(helix=0, forward=True, start=0, end=12) - scaf1_ss = sc.Domain(helix=1, forward=False, start=0, end=12) - scaf = sc.Strand(domains=[scaf0_ss, scaf1_ss]) - - first_stap0_left_ss = sc.Domain(helix=0, forward=False, start=0, end=3) - first_stap1_ss = sc.Domain(helix=1, forward=True, start=0, end=6) - first_stap0_right_ss = sc.Domain(helix=0, forward=False, start=3, end=6) - first_stap = sc.Strand(domains=[first_stap0_left_ss, first_stap1_ss, first_stap0_right_ss]) - - second_stap1_right_ss = sc.Domain(helix=1, forward=True, start=9, end=12) - second_stap0_ss = sc.Domain(helix=0, forward=False, start=6, end=12) - second_stap1_left_ss = sc.Domain(helix=1, forward=True, start=6, end=9) - second_stap = sc.Strand(domains=[second_stap1_right_ss, second_stap0_ss, second_stap1_left_ss]) - - strands = [scaf, first_stap, second_stap] - design = sc.Design(grid=sc.square, strands=strands) - design.assign_dna(scaf, 'ACC TAA GAA AAC ACT CAT GAA ATC'.replace(' ', '')) - self.assertEqual('GGT GAT TTC TTA'.replace(' ', ''), first_stap.dna_sequence) - self.assertEqual('AGT GTT TTC ATG'.replace(' ', ''), second_stap.dna_sequence) - - def test_assign_dna__upper_left_edge_staple_of_16H_origami_rectangle(self) -> None: - """ - staple | - | | - +-GATTTTGTGAGTAGAA- | - -CTAAAACACTCATCTT--+ - """ - scaf0_ss = sc.Domain(helix=0, forward=True, start=0, end=16) - scaf1_ss = sc.Domain(helix=1, forward=False, start=0, end=16) - stap0_ss = sc.Domain(helix=0, forward=False, start=0, end=16) - stap1_ss = sc.Domain(helix=1, forward=True, start=0, end=16) - scaf = sc.Strand(domains=[scaf1_ss, scaf0_ss]) - stap = sc.Strand(domains=[stap1_ss, stap0_ss]) - strands = [scaf, stap] - design = sc.Design(grid=sc.square, strands=strands) - - seq_m13_upper_left = 'AAGATGAGTGTTTTAGTGTATTCTTTTGCCTC' - design.assign_dna(scaf, seq_m13_upper_left) - expected_seq_stap_upperleft = 'CTAAAACACTCATCTTGAGGCAAAAGAATACA' - self.assertEqual(expected_seq_stap_upperleft, stap.dna_sequence) - - def test_assign_dna__2helix_with_deletions(self) -> None: - r""" - scaf index: 2 3 4 5 - offset: 0 D1 2 3 D4 5 - + - - + - /C A T C\ - | G T A G | - helix 0 | < + + ] | - | | | | - helix 1 | [ + + > | - | T T A C | - \A A T G/ - + ] < + - offset: 0 D1 2 3 D4 5 - scaf index: 1 0 7 6 - """ - width = 6 - width_h = width // 2 - helices = [sc.Helix(max_offset=width), sc.Helix(max_offset=width)] - stap_left_ss1 = sc.Domain(1, True, 0, width_h) - stap_left_ss0 = sc.Domain(0, False, 0, width_h) - stap_right_ss0 = sc.Domain(0, False, width_h, width) - stap_right_ss1 = sc.Domain(1, True, width_h, width) - scaf_ss1_left = sc.Domain(1, False, 0, width_h) - scaf_ss0 = sc.Domain(0, True, 0, width) - scaf_ss1_right = sc.Domain(1, False, width_h, width) - stap_left = sc.Strand([stap_left_ss1, stap_left_ss0]) - stap_right = sc.Strand([stap_right_ss0, stap_right_ss1]) - scaf = sc.Strand([scaf_ss1_left, scaf_ss0, scaf_ss1_right], color=sc.default_scaffold_color) - strands = [stap_left, stap_right, scaf] - design = sc.Design(helices=helices, strands=strands, grid=sc.square) - design.add_deletion(helix=0, offset=1) - design.add_deletion(helix=0, offset=4) - design.add_deletion(helix=1, offset=1) - design.add_deletion(helix=1, offset=4) - design.assign_dna(scaf, 'AACATCGT') - self.assertEqual("AACATCGT", scaf.dna_sequence) - self.assertEqual("TTTG", stap_left.dna_sequence) - self.assertEqual("GAAC", stap_right.dna_sequence) - - def test_assign_dna__wildcards_simple(self) -> None: - """ - 012 345 678 - -TTC> -GGA> -CCT> - None: - """ - 0 16 - AACGTATCGCGATGCATCC - 0 [-------I: 3-------> - <-------I: 3-------] - """ - design = sc.Design(grid=sc.square, helices=[sc.Helix(max_offset=100)], strands=[]) - design.draw_strand(0, 0).move(16) - design.draw_strand(0, 16).move(-16) - design.add_insertion(0, 8, 3) - design.assign_dna(strand=design.strands[0], sequence='AACGTATCGCGATGCATCC', assign_complement=True) - - """ - 0 16 - AACGTATCGCGATGCATCC - 0 [-------I: 3-------> - <-------I: 3-------] - TTGCATAGCGCTACGTAGG - """ - self.assertEqual(design.strands[0].dna_sequence, 'AACGTATCGCGATGCATCC') - self.assertEqual(design.strands[1].dna_sequence, 'GGATGCATCGCGATACGTT') - - def test_assign_dna__two_bound_strands__with_insertions__complement_true(self) -> None: - """ - 0 16 - - 0 [-------I: 3------> - <---]<--I: 3------] - TTGCATAGCGCTACGTAGG - """ - design = sc.Design(grid=sc.square, helices=[sc.Helix(max_offset=100)], strands=[]) - design.draw_strand(0, 0).move(16) - design.draw_strand(0, 5).move(-5) - design.draw_strand(0, 16).move(-11) - design.add_insertion(0, 8, 3) - design.assign_dna(strand=design.strands[1], sequence='ACGTT', assign_complement=True) - design.assign_dna(strand=design.strands[2], sequence='GGATGCATCGCGAT', assign_complement=True) - - """ - 0 16 - AACGTATCGCGATGCATCC - 0 [-------I: 3------> - <---]<--I: 3------] - TTGCATAGCGCTACGTAGG - """ - self.assertEqual(design.strands[0].dna_sequence, 'AACGTATCGCGATGCATCC') - self.assertEqual(design.strands[1].dna_sequence, 'ACGTT') - self.assertEqual(design.strands[2].dna_sequence, 'GGATGCATCGCGAT') - - def test_assign_dna__one_bound_strand__with_deletions__complement_true(self) -> None: - """ - 0 16 - AACGTACG TGCATCC - 0 [-------X------> - <-------X------] - """ - design = sc.Design(grid=sc.square, helices=[sc.Helix(max_offset=100)], strands=[]) - design.draw_strand(0, 0).move(16) - design.draw_strand(0, 16).move(-16) - design.add_deletion(0, 8) - design.assign_dna(strand=design.strands[0], sequence='AACGTACGTGCATCC', assign_complement=True) - - """ - 0 16 - AACGTACG TGCATCC - 0 [-------X------> - <-------X------] - TTGCATGC ACGTAGG - """ - self.assertEqual(design.strands[0].dna_sequence, 'AACGTACGTGCATCC') - self.assertEqual(design.strands[1].dna_sequence, 'GGATGCACGTACGTT') - - def test_assign_dna__two_bound_strands__with_deletions__complement_true(self) -> None: - """ - 0 16 - 0 [-------X------> - <---]<--X------] - TTGCATAG GCTACGT - """ - design = sc.Design(grid=sc.square, helices=[sc.Helix(max_offset=100)], strands=[]) - design.draw_strand(0, 0).move(16) - design.draw_strand(0, 5).move(-5) - design.draw_strand(0, 16).move(-11) - design.add_deletion(0, 8) - design.assign_dna(strand=design.strands[1], sequence='ACGTT', assign_complement=True) - design.assign_dna(strand=design.strands[2], sequence='TGCATCGGAT', assign_complement=True) - - """ - 0 16 - AACGTATC CGATGCA - 0 [-------X------> - <---]<--X------] - TTGCATAG GCTACGT - """ - self.assertEqual(design.strands[0].dna_sequence, 'AACGTATCCGATGCA') - self.assertEqual(design.strands[1].dna_sequence, 'ACGTT') - self.assertEqual(design.strands[2].dna_sequence, 'TGCATCGGAT') - - -class TestAssignDNAToDomains(unittest.TestCase): - - def setUp(self) -> None: - """ - 012 345 678 901 234 567 890 - +---------------+ - | | - | +-----|-------+ +-+ - | | | | | | - -ACG> +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA> - None: - """ - 012 345 678 901 234 567 890 - +---------------+ - | | - | +-----|-------+ +-+ - | | | | | | - -ACG> +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA> - None: - with self.assertRaises(sc.IllegalDesignError): - self.design.assign_dna(self.strand_top_big9, 'AACTTC', domain=self.dom_top9) - - def test_assign_dna__to_individual_domains__wildcards_multiple_overlaps(self) -> None: - """ - 012 345 678 901 234 567 890 - +---------------+ - | | - | +-----|-------+ +-+ - | | | | | | - -ACG> +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA> - +???> -???+ -AAC+ -???> +???+ +???> - +TTC> -???+ -AAC+ -???> +???+ +???> - +TTC> -???+ -AAC+ -TGC> +???+ +???> - +TTC> -???+ -AAC+ -TGC> +???+ +???> - +TTC> -???+ -AAC+ -TGC> +TTG+ +???> - +TTC> -???+ -AAC+ -TGC> +TTG+ +GCA> - +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA> - None: - """ - 012 345 678 901 234 567 890 - +---------------+ - | | - | +-----|-------+ +-+ - | | | | | | - -ACG> +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA> - - -TGC> - +TTC> -AAC+ -TGC> - +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA> - None: - ss0 = sc.Domain(0, True, 0, 10) - ss1 = sc.Domain(1, False, 0, 10) - strand = sc.Strand([ss0, ss1]) - strand.set_dna_sequence("AAAACCCCGGGGTTTTACGT") - # offset: 0 1 2 3 4 5 6 7 8 9 - # index: 0 1 2 3 4 5 6 7 8 9 - # A A A A C C C C G G - # helix 0 [ - - - - - - - - + - # | - # helix 1 < - - - - - - - - + - # T G C A T T T T G G - # offset: 0 1 2 3 4 5 6 7 8 9 - # index: 19 18 17 16 15 14 13 12 11 10 - self.assertEqual("A", ss0.dna_sequence_in(0, 0)) - self.assertEqual("AA", ss0.dna_sequence_in(0, 1)) - self.assertEqual("AAA", ss0.dna_sequence_in(0, 2)) - self.assertEqual("AAAA", ss0.dna_sequence_in(0, 3)) - self.assertEqual("AAAAC", ss0.dna_sequence_in(0, 4)) - self.assertEqual("AAAACC", ss0.dna_sequence_in(0, 5)) - self.assertEqual("AAAACCC", ss0.dna_sequence_in(0, 6)) - self.assertEqual("AAAACCCC", ss0.dna_sequence_in(0, 7)) - self.assertEqual("AAAACCCCG", ss0.dna_sequence_in(0, 8)) - self.assertEqual("AAAACCCCGG", ss0.dna_sequence_in(0, 9)) - # - self.assertEqual("G", ss1.dna_sequence_in(9, 9)) - self.assertEqual("GG", ss1.dna_sequence_in(8, 9)) - self.assertEqual("GGT", ss1.dna_sequence_in(7, 9)) - self.assertEqual("GGTT", ss1.dna_sequence_in(6, 9)) - self.assertEqual("GGTTT", ss1.dna_sequence_in(5, 9)) - self.assertEqual("GGTTTT", ss1.dna_sequence_in(4, 9)) - self.assertEqual("GGTTTTA", ss1.dna_sequence_in(3, 9)) - self.assertEqual("GGTTTTAC", ss1.dna_sequence_in(2, 9)) - self.assertEqual("GGTTTTACG", ss1.dna_sequence_in(1, 9)) - self.assertEqual("GGTTTTACGT", ss1.dna_sequence_in(0, 9)) - - def test_dna_sequence_in__right_then_left_deletions(self) -> None: - ss0 = sc.Domain(0, True, 0, 10, deletions=[2, 5, 6]) - ss1 = sc.Domain(1, False, 0, 10, deletions=[2, 6, 7]) - strand = sc.Strand([ss0, ss1]) - strand.set_dna_sequence("AAACCGGGGTTAGT") - # offset: 0 1 D2 3 4 D5 D6 7 8 9 - # index: 0 1 2 3 4 5 6 - # A A A C C G G - # helix 0 [ - - - - - - - - + - # | - # helix 1 < - - - - - - - - + - # T G A T T G G - # offset: 0 1 D2 3 4 5 D6 D7 8 9 - # index: 13 12 11 10 9 9 7 - self.assertEqual("A", ss0.dna_sequence_in(0, 0)) - self.assertEqual("AA", ss0.dna_sequence_in(0, 1)) - self.assertEqual("AA", ss0.dna_sequence_in(0, 2)) - self.assertEqual("AAA", ss0.dna_sequence_in(0, 3)) - self.assertEqual("AAAC", ss0.dna_sequence_in(0, 4)) - self.assertEqual("AAAC", ss0.dna_sequence_in(0, 5)) - self.assertEqual("AAAC", ss0.dna_sequence_in(0, 6)) - self.assertEqual("AAACC", ss0.dna_sequence_in(0, 7)) - self.assertEqual("AAACCG", ss0.dna_sequence_in(0, 8)) - self.assertEqual("AAACCGG", ss0.dna_sequence_in(0, 9)) - # - self.assertEqual("G", ss1.dna_sequence_in(9, 9)) - self.assertEqual("GG", ss1.dna_sequence_in(8, 9)) - self.assertEqual("GG", ss1.dna_sequence_in(7, 9)) - self.assertEqual("GG", ss1.dna_sequence_in(6, 9)) - self.assertEqual("GGT", ss1.dna_sequence_in(5, 9)) - self.assertEqual("GGTT", ss1.dna_sequence_in(4, 9)) - self.assertEqual("GGTTA", ss1.dna_sequence_in(3, 9)) - self.assertEqual("GGTTA", ss1.dna_sequence_in(2, 9)) - self.assertEqual("GGTTAG", ss1.dna_sequence_in(1, 9)) - self.assertEqual("GGTTAGT", ss1.dna_sequence_in(0, 9)) - - # if TEST_OFFSETS_AT_DELETION_INSERTIONS: - # self.assertEqual("AA", ss0.dna_sequence_in(0, 3)) - # self.assertEqual("AAACC", ss0.dna_sequence_in(0, 7)) - # self.assertEqual("GGT", ss1.dna_sequence_in(6, 10)) - # self.assertEqual("GGTTTA", ss1.dna_sequence_in(2, 10)) - - def test_dna_sequence_in__right_then_left_insertions(self) -> None: - ss0 = sc.Domain(0, True, 0, 10, insertions=[(2, 1), (6, 2)]) - ss1 = sc.Domain(1, False, 0, 10, insertions=[(2, 1), (6, 2)]) - strand = sc.Strand([ss0, ss1]) - strand.set_dna_sequence("AAAACCCCGGGGTTTTACGTACGTAC") - # offset: 0 1 2 I 3 4 5 6 I I 7 8 9 - # index: 0 1 2 3 4 5 6 7 8 9 10 11 12 - # A A A A C C C C G G G G T - # helix 0 [ - - - - - - - - - - - + - # | - # helix 1 < - - - - - - - - - - - + - # C A T G C A T G C A T T T - # offset: 0 1 2 I 3 4 5 6 I I 7 8 9 - # index: 25 24 23 22 21 20 19 18 17 16 15 14 13 - self.assertEqual("A", ss0.dna_sequence_in(0, 0)) - self.assertEqual("AA", ss0.dna_sequence_in(0, 1)) - self.assertEqual("AAAA", ss0.dna_sequence_in(0, 2)) - self.assertEqual("AAAAC", ss0.dna_sequence_in(0, 3)) - self.assertEqual("AAAACC", ss0.dna_sequence_in(0, 4)) - self.assertEqual("AAAACCC", ss0.dna_sequence_in(0, 5)) - self.assertEqual("AAAACCCCGG", ss0.dna_sequence_in(0, 6)) - self.assertEqual("AAAACCCCGGG", ss0.dna_sequence_in(0, 7)) - self.assertEqual("AAAACCCCGGGG", ss0.dna_sequence_in(0, 8)) - self.assertEqual("AAAACCCCGGGGT", ss0.dna_sequence_in(0, 9)) - # - self.assertEqual("T", ss1.dna_sequence_in(9, 9)) - self.assertEqual("TT", ss1.dna_sequence_in(8, 9)) - self.assertEqual("TTT", ss1.dna_sequence_in(7, 9)) - self.assertEqual("TTTACG", ss1.dna_sequence_in(6, 9)) - self.assertEqual("TTTACGT", ss1.dna_sequence_in(5, 9)) - self.assertEqual("TTTACGTA", ss1.dna_sequence_in(4, 9)) - self.assertEqual("TTTACGTAC", ss1.dna_sequence_in(3, 9)) - self.assertEqual("TTTACGTACGT", ss1.dna_sequence_in(2, 9)) - self.assertEqual("TTTACGTACGTA", ss1.dna_sequence_in(1, 9)) - self.assertEqual("TTTACGTACGTAC", ss1.dna_sequence_in(0, 9)) - - # if TEST_OFFSETS_AT_DELETION_INSERTIONS: - # self.assertEqual("AAAA", ss0.dna_sequence_in(0, 3)) - # self.assertEqual("AAAACCCCGG", ss0.dna_sequence_in(0, 7)) - # self.assertEqual("TTTACG", ss1.dna_sequence_in(6, 10)) - # self.assertEqual("TTTACGTACGT", ss1.dna_sequence_in(2, 10)) - - def test_dna_sequence_in__right_then_left_deletions_and_insertions(self) -> None: - ss0 = sc.Domain(0, True, 0, 10, deletions=[4], insertions=[(2, 1), (6, 2)]) - ss1 = sc.Domain(1, False, 0, 10, deletions=[4], insertions=[(2, 1), (6, 2)]) - strand = sc.Strand([ss0, ss1]) - strand.set_dna_sequence("AAAACCCCGGGGTTTTACGTACGT") - # offset: 0 1 2 I 3 D4 5 6 I I 7 8 9 - # index: 0 1 2 3 4 5 6 7 8 9 10 11 - # A A A A C C C C G G G G - # helix 0 [ - - - - - - - - - - - + - # | - # helix 1 < - - - - - - - - - - - + - # T G C A T G C A T T T T - # offset: 0 1 2 I 3 D4 5 6 I I 7 8 9 - # index: 23 22 21 20 19 18 17 16 15 14 13 12 - self.assertEqual("AA", ss0.dna_sequence_in(2, 2)) - self.assertEqual("CCG", ss0.dna_sequence_in(6, 6)) - self.assertEqual("TAC", ss1.dna_sequence_in(6, 6)) - self.assertEqual("AC", ss1.dna_sequence_in(2, 2)) - # - self.assertEqual("A ".strip(), ss0.dna_sequence_in(0, 0)) - self.assertEqual("AA ".strip(), ss0.dna_sequence_in(0, 1)) - self.assertEqual("AAAA ".strip(), ss0.dna_sequence_in(0, 2)) - self.assertEqual("AAAAC ".strip(), ss0.dna_sequence_in(0, 3)) - self.assertEqual("AAAAC ".strip(), ss0.dna_sequence_in(0, 4)) - self.assertEqual("AAAACC ".strip(), ss0.dna_sequence_in(0, 5)) - self.assertEqual("AAAACCCCG ".strip(), ss0.dna_sequence_in(0, 6)) - self.assertEqual("AAAACCCCGG ".strip(), ss0.dna_sequence_in(0, 7)) - self.assertEqual("AAAACCCCGGG ".strip(), ss0.dna_sequence_in(0, 8)) - self.assertEqual("AAAACCCCGGGG".strip(), ss0.dna_sequence_in(0, 9)) - self.assertEqual(" AAACCCCGGGG".strip(), ss0.dna_sequence_in(1, 9)) - self.assertEqual(" AACCCCGGGG".strip(), ss0.dna_sequence_in(2, 9)) - self.assertEqual(" CCCCGGGG".strip(), ss0.dna_sequence_in(3, 9)) - self.assertEqual(" CCCGGGG".strip(), ss0.dna_sequence_in(4, 9)) - self.assertEqual(" CCCGGGG".strip(), ss0.dna_sequence_in(5, 9)) - self.assertEqual(" CCGGGG".strip(), ss0.dna_sequence_in(6, 9)) - self.assertEqual(" GGG".strip(), ss0.dna_sequence_in(7, 9)) - self.assertEqual(" GG".strip(), ss0.dna_sequence_in(8, 9)) - self.assertEqual(" G".strip(), ss0.dna_sequence_in(9, 9)) - # strip() below is so auto-formatting preserves nice lineup of characters - self.assertEqual("T ".strip(), ss1.dna_sequence_in(9, 9)) - self.assertEqual("TT ".strip(), ss1.dna_sequence_in(8, 9)) - self.assertEqual("TTT ".strip(), ss1.dna_sequence_in(7, 9)) - self.assertEqual("TTTTAC ".strip(), ss1.dna_sequence_in(6, 9)) - self.assertEqual("TTTTACG ".strip(), ss1.dna_sequence_in(5, 9)) - self.assertEqual("TTTTACG ".strip(), ss1.dna_sequence_in(4, 9)) - self.assertEqual("TTTTACGT ".strip(), ss1.dna_sequence_in(3, 9)) - self.assertEqual("TTTTACGTAC ".strip(), ss1.dna_sequence_in(2, 9)) - self.assertEqual("TTTTACGTACG ".strip(), ss1.dna_sequence_in(1, 9)) - self.assertEqual("TTTTACGTACGT".strip(), ss1.dna_sequence_in(0, 9)) - self.assertEqual(" TTTACGTACGT".strip(), ss1.dna_sequence_in(0, 8)) - self.assertEqual(" TTACGTACGT".strip(), ss1.dna_sequence_in(0, 7)) - self.assertEqual(" TACGTACGT".strip(), ss1.dna_sequence_in(0, 6)) - self.assertEqual(" GTACGT".strip(), ss1.dna_sequence_in(0, 5)) - self.assertEqual(" TACGT".strip(), ss1.dna_sequence_in(0, 4)) - self.assertEqual(" TACGT".strip(), ss1.dna_sequence_in(0, 3)) - self.assertEqual(" ACGT".strip(), ss1.dna_sequence_in(0, 2)) - self.assertEqual(" GT".strip(), ss1.dna_sequence_in(0, 1)) - self.assertEqual(" T".strip(), ss1.dna_sequence_in(0, 0)) - - # if TEST_OFFSETS_AT_DELETION_INSERTIONS: - # self.assertEqual("AAAA", ss0.dna_sequence_in(0, 3)) - # self.assertEqual("AAAAC", ss0.dna_sequence_in(0, 5)) - # self.assertEqual("AAAACCCCGG", ss0.dna_sequence_in(0, 7)) - # self.assertEqual("TTTACG", ss1.dna_sequence_in(6, 10)) - # self.assertEqual("TTTTACG", ss1.dna_sequence_in(4, 10)) - # self.assertEqual("TTTACGTACGT", ss1.dna_sequence_in(2, 10)) - - -class TestOxviewExport(unittest.TestCase): - def test_export(self): - "Ensures that OxView export matches OxDNA export." - # Uses the basic design from OxdnaExport - helices = [sc.Helix(max_offset=7), sc.Helix(max_offset=7)] - design = sc.Design(helices=helices, grid=sc.square) - design.draw_strand(0, 0).move(7).cross(1).move(-7).with_color( - sc.Color(254, 123, 222)) - design.draw_strand(0, 7).move(-7).cross(1).move(7) - - oxdna_system = _convert_design_to_oxdna_system(design) - - oxv = design.to_oxview_format(use_strand_colors=True) - oxv_no_color = design.to_oxview_format(use_strand_colors=False) - - # Is the box correct? - self.assertEqual(list(oxdna_system.compute_bounding_box()), oxv['box']) - - # Do we have the same number of strands? - self.assertEqual(len(oxdna_system.strands), - len(oxv['systems'][0]['strands'])) - - for i, (oxdna_strand, oxview_strand, oxview_nocolor_strand, - des_strand) in enumerate( - zip(oxdna_system.strands, oxv['systems'][0]['strands'], - oxv_no_color['systems'][0]['strands'], - design.strands)): - self.assertEqual(i + 1, oxview_strand['id']) - - if des_strand.color: - scolor = des_strand.color.to_cadnano_v2_int_hex() - else: - scolor = None - - self.assertEqual(len(oxdna_strand.nucleotides), - len(oxview_strand['monomers'])) - for j, (oxdna_nt, oxview_nt, oxview_nocolor_nt) in enumerate( - zip(oxdna_strand.nucleotides, oxview_strand['monomers'], - oxview_nocolor_strand['monomers'])): - self.assertListEqual(list(oxdna_nt.r), oxview_nt['p']) - self.assertListEqual(list(oxdna_nt.b), oxview_nt['a1']) - self.assertListEqual(list(oxdna_nt.n), oxview_nt['a3']) - if scolor is not None: - self.assertEqual(scolor, oxview_nt['color']) - self.assertNotIn('color', oxview_nocolor_nt) - else: - self.assertNotIn('color', oxview_nt) - self.assertNotIn('color', oxview_nocolor_nt) - self.assertEqual(oxdna_nt.base, oxview_nt['type']) - - def test_bp(self): - des = sc.Design() - des.set_grid(sc.Grid.square) - des.helices = {i: sc.Helix(max_offset=20, idx=i, grid_position=(0, i)) for i in range(3)} - des.draw_strand(0, 0).to(6).with_deletions(4).to(15).cross(1, 9).to(20).with_insertions( - (15, 2)).cross(0).to(9) - des.draw_strand(1, 0).to(9).cross(0).to(0).with_deletions(4) - des.draw_strand(1, 20).to(2).with_insertions((15, 2)).cross(2, 0).to(20).with_sequence( - 'TTTCTCATGGGAAGCAAACTCGGTTTCCGCGTCGGATAGT') - des.draw_strand(2, 8).to(5).loopout(2, 5, 4).to(0) - des.draw_strand(2, 20).extension_5p(8).to(12).extension_3p(8).with_sequence( - 'ATACTGGAACTACGCGCGTGAATT', assign_complement=False) - - oxv = des.to_oxview_format() - - strands = oxv['systems'][0]['strands'] - - # Basic complements with a deletion (wildcard sequences) - for i in range(0, 8): - self.assertEqual(strands[0]['monomers'][i]['bp'], strands[1]['monomers'][-i - 1]['id']) - self.assertEqual(strands[1]['monomers'][-i - 1]['bp'], strands[0]['monomers'][i]['id']) - - # Self-complementary strand (wildcard sequences) - for i in range(8, 14): - self.assertEqual(strands[0]['monomers'][i]['bp'], strands[0]['monomers'][7 - i]['id']) - - # Insertion (defined sequences) - for i in range(14, 27): - self.assertEqual(strands[0]['monomers'][i]['bp'], strands[2]['monomers'][26 - i]['id']) - - # Before, in, and after a loopout (one strand with no sequence, one with defined sequence) - for i in range(0, 3): - self.assertEqual(strands[3]['monomers'][i]['bp'], strands[2]['monomers'][27 - i]['id']) - - for i in range(3, 8): - self.assertNotIn('bp', strands[3]['monomers'][i]) - - for i in range(8, 12): - self.assertEqual(strands[3]['monomers'][i]['bp'], strands[2]['monomers'][23 + 8 - i]['id']) - - # Mismatches should not be paired; also, extensions: - for i in range(0, 8): # 5p extension - self.assertNotIn('bp', strands[4]['monomers'][i]) - for i in range(8, 12): # complementary - print(i) - self.assertEqual(strands[4]['monomers'][i]['bp'], strands[2]['monomers'][40 + 7 - i]['id']) - for i in range(12, 14): # two mismatches - self.assertNotIn('bp', strands[4]['monomers'][i]) - self.assertNotIn('bp', strands[2]['monomers'][32 + 15 - i]) - for i in range(14, 16): # complementary again - self.assertEqual(strands[4]['monomers'][i]['bp'], strands[2]['monomers'][32 + 15 - i]['id']) - for i in range(16, len(strands[4]['monomers'])): # 3p extension - self.assertNotIn('bp', strands[4]['monomers'][i]) - - # Unbound region - for i in range(28, 32): - self.assertNotIn('bp', strands[2]['monomers'][i]) - - def test_export_file(self): - "Ensures that file export works, and writes a suitable JSON file that matches the output." - self.maxDiff = None - helices = [sc.Helix(max_offset=7), sc.Helix(max_offset=7)] - design = sc.Design(helices=helices, grid=sc.square) - design.draw_strand(0, 0).move(7).cross(1).move(-7).with_color( - sc.Color(254, 123, 222)) - design.draw_strand(0, 7).move(-7).cross(1).move(7) - - oxv = design.to_oxview_format(use_strand_colors=True) - - with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: - design.write_oxview_file(filename=f.name) - filename = f.name - with open(filename, 'r') as f2: - oxv2 = json.load(f2) - os.unlink(filename) - - # The dates won't be equal, so delete them - del oxv['date'] - del oxv2['date'] - - self.assertEqual(oxv, oxv2) - - -class TestOxdnaExport(unittest.TestCase): - def setUp(self) -> None: - self.OX_UNITS_TO_NM = 0.8518 - self.NM_TO_OX_UNITS = 1.0 / self.OX_UNITS_TO_NM - self.OX_BASE_DIST = 0.6 - self.BASES_PER_TURN = 10.5 - self.HELIX_ANGLE = math.pi * 2 / self.BASES_PER_TURN - self.RISE_PER_BASE_PAIR = 0.332 - # square of expected distance between adjacent nucleotide centers of mass - self.EXPECTED_ADJ_NUC_CM_DIST2 = (2 * self.OX_BASE_DIST * math.sin(self.HELIX_ANGLE / 2)) ** 2 + ( - self.RISE_PER_BASE_PAIR * self.NM_TO_OX_UNITS) ** 2 - - def test_basic_design(self) -> None: - """ 2 double strands of length 7 connected across helices. - 0 7 - helix 0 [------+ - +------]\ - | | - helix 1 +------>/ - <------+ - """ - helices = [sc.Helix(max_offset=7), sc.Helix(max_offset=7)] - design = sc.Design(helices=helices, grid=sc.square) - design.draw_strand(0, 0).move(7).cross(1).move(-7) - design.draw_strand(0, 7).move(-7).cross(1).move(7) - - # expected values for verification - expected_num_nucleotides = 7 * 4 - expected_strand_length = 7 * 2 - - dat, top = design.to_oxdna_format() - dat_lines = dat.strip().split('\n') - top_lines = top.strip().split('\n') - - # check length of output files are as expected (matches # of nucleotides plus header size) - self.assertEqual(expected_num_nucleotides + 3, len(dat_lines)) - self.assertEqual(expected_num_nucleotides + 1, len(top_lines)) - - # find relevant values for nucleotides - cm_poss = [] # center of mass position - nbrs_3p = [] - nbrs_5p = [] - - for line in dat_lines[3:]: - data = line.strip().split() - # make sure there are 15 values per line (3 values per vector * 5 vectors per line) - # order of vectors: center of mass position, backbone base versor, normal versor, velocity, angular velocity (more info on versors: https://eater.net/quaternions) - self.assertEqual(15, len(data)) - - cm_poss.append(tuple([float(x) for x in data[0:3]])) - bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector - nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector - - # make sure normal vectors and backbone vectors are unit length - sqr_bb_vec = sum([x ** 2 for x in bb_vec]) - sqr_nm_vec = sum([x ** 2 for x in nm_vec]) - self.assertAlmostEqual(1.0, sqr_bb_vec) - self.assertAlmostEqual(1.0, sqr_nm_vec) - - for value in data[9:]: # values for velocity and angular velocity vectors are 0 - self.assertAlmostEqual(0, float(value)) - - strand1_idxs = [] - strand2_idxs = [] - for nuc_idx, line in enumerate(top_lines[1:]): - data = line.strip().split() - # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor - self.assertEqual(4, len(data)) - - # make sure there are only 2 strands - strand_num = int(data[0]) - self.assertIn(strand_num, [1, 2]) - # make sure base is valid - base = data[1] - self.assertIn(base, ['A', 'C', 'G', 'T']) - - nbrs_3p.append(int(data[2])) - nbrs_5p.append(int(data[3])) - - # append start of strand (no 5' neighbor) to list of indexes for strand - neighbor_5 = int(data[3]) - if neighbor_5 == -1: - if strand_num == 1: - strand1_start = nuc_idx - strand1_idxs.append(strand1_start) - else: - strand2_start = nuc_idx - strand2_idxs.append(strand2_start) - - # reconstruct strands using indices from oxDNA files - next_idx = nbrs_3p[strand1_start] - while next_idx >= 0: - strand1_idxs.append(next_idx) - next_idx = nbrs_3p[strand1_idxs[-1]] - - next_idx = nbrs_3p[strand2_start] - while next_idx >= 0: - strand2_idxs.append(next_idx) - next_idx = nbrs_3p[strand2_idxs[-1]] - - # assert that strands are the correct length - self.assertEqual(expected_strand_length, len(strand1_idxs)) - self.assertEqual(expected_strand_length, len(strand2_idxs)) - - for i in range(expected_strand_length - 1): - # ignore nucleotide distance between domains (on crossover) - if i == 6: - continue - - strand1_nuc_idx1 = strand1_idxs[i] - strand1_nuc_idx2 = strand1_idxs[i + 1] - strand2_nuc_idx1 = strand2_idxs[i] - strand2_nuc_idx2 = strand2_idxs[i + 1] - - # find the center of mass for adjacent nucleotides - s1_cmp1 = cm_poss[strand1_nuc_idx1] - s1_cmp2 = cm_poss[strand1_nuc_idx2] - s2_cmp1 = cm_poss[strand2_nuc_idx1] - s2_cmp2 = cm_poss[strand2_nuc_idx2] - - # calculate and verify squared distance between adjacent nucleotides in a domain - diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) - diff2 = tuple([s2_cmp1[j] - s2_cmp2[j] for j in range(3)]) - sqr_dist1 = sum([x ** 2 for x in diff1]) - sqr_dist2 = sum([x ** 2 for x in diff2]) - - self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) - self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist2) - - def test_helix_groups(self) -> None: - """ - 2 double strands of length 7 connected across helices. - - honeycomb square - group a group b - - 01234567 01234567 - helix 0 [------+ helix 2 - | +------] - | | - helix 1 | helix 3 +------> - <------+ - - - Other than placing second strand on new helices in new helix group, same design as basic design, - so testing the same things essentially. - """ - helices = [sc.Helix(max_offset=7, group='a'), sc.Helix(max_offset=7, group='a'), - sc.Helix(max_offset=7, group='b'), sc.Helix(max_offset=7, group='b')] - groups = { - 'a': sc.HelixGroup(position=sc.Position3D(0, 0, 0), grid=sc.honeycomb), - 'b': sc.HelixGroup(position=sc.Position3D(100, 0, 0), grid=sc.square), - } - design = sc.Design(helices=helices, groups=groups) - design.draw_strand(0, 0).move(7).cross(1).move(-7) - design.draw_strand(2, 7).move(-7).cross(3).move(7) # unlike basic design, put strand on helices 2,3 - - # expected values for verification - expected_num_nucleotides = 7 * 4 - expected_strand_length = 7 * 2 - - dat, top = design.to_oxdna_format() - dat_lines = dat.strip().split('\n') - top_lines = top.strip().split('\n') - - # check length of output files are as expected (matches # of nucleotides plus header size) - self.assertEqual(expected_num_nucleotides + 3, len(dat_lines)) - self.assertEqual(expected_num_nucleotides + 1, len(top_lines)) - - # find relevant values for nucleotides - cm_poss = [] # center of mass position - nbrs_3p = [] - nbrs_5p = [] - - for line in dat_lines[3:]: - data = line.strip().split() - # make sure there are 15 values per line (3 values per vector * 5 vectors per line) - # order of vectors: center of mass position, backbone base versor, normal versor, velocity, angular velocity (more info on versors: https://eater.net/quaternions) - self.assertEqual(15, len(data)) - - cm_poss.append(tuple([float(x) for x in data[0:3]])) - bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector - nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector - - # make sure normal vectors and backbone vectors are unit length - sqr_bb_vec = sum([x ** 2 for x in bb_vec]) - sqr_nm_vec = sum([x ** 2 for x in nm_vec]) - self.assertAlmostEqual(1.0, sqr_bb_vec) - self.assertAlmostEqual(1.0, sqr_nm_vec) - - for value in data[9:]: # values for velocity and angular velocity vectors are 0 - self.assertAlmostEqual(0, float(value)) - - strand1_idxs = [] - strand2_idxs = [] - for nuc_idx, line in enumerate(top_lines[1:]): - data = line.strip().split() - # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor - self.assertEqual(4, len(data)) - - # make sure there are only 2 strands - strand_num = int(data[0]) - self.assertIn(strand_num, [1, 2]) - # make sure base is valid - base = data[1] - self.assertIn(base, ['A', 'C', 'G', 'T']) - - nbrs_3p.append(int(data[2])) - nbrs_5p.append(int(data[3])) - - # append start of strand (no 5' neighbor) to list of indexes for strand - neighbor_5 = int(data[3]) - if neighbor_5 == -1: - if strand_num == 1: - strand1_start = nuc_idx - strand1_idxs.append(strand1_start) - else: - strand2_start = nuc_idx - strand2_idxs.append(strand2_start) - - # reconstruct strands using indices from oxDNA files - next_idx = nbrs_3p[strand1_start] - while next_idx >= 0: - strand1_idxs.append(next_idx) - next_idx = nbrs_3p[strand1_idxs[-1]] - - next_idx = nbrs_3p[strand2_start] - while next_idx >= 0: - strand2_idxs.append(next_idx) - next_idx = nbrs_3p[strand2_idxs[-1]] - - # assert that strands are the correct length - self.assertEqual(expected_strand_length, len(strand1_idxs)) - self.assertEqual(expected_strand_length, len(strand2_idxs)) - - for i in range(expected_strand_length - 1): - # ignore nucleotide distance between domains (on crossover) - if i == 6: - continue - - strand1_nuc_idx1 = strand1_idxs[i] - strand1_nuc_idx2 = strand1_idxs[i + 1] - strand2_nuc_idx1 = strand2_idxs[i] - strand2_nuc_idx2 = strand2_idxs[i + 1] - - # find the center of mass for adjacent nucleotides - s1_cmp1 = cm_poss[strand1_nuc_idx1] - s1_cmp2 = cm_poss[strand1_nuc_idx2] - s2_cmp1 = cm_poss[strand2_nuc_idx1] - s2_cmp2 = cm_poss[strand2_nuc_idx2] - - # calculate and verify squared distance between adjacent nucleotides in a domain - diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) - diff2 = tuple([s2_cmp1[j] - s2_cmp2[j] for j in range(3)]) - sqr_dist1 = sum([x ** 2 for x in diff1]) - sqr_dist2 = sum([x ** 2 for x in diff2]) - - self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) - self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist2) - - def test_honeycomb_design(self) -> None: - """ A single strand on a honeycomb grid. - 0 8 - helix 0 [-------+ - | - helix 1 +-------- - | - helix 2 +-------> - """ - helices = [sc.Helix(grid_position=(1, 1), max_offset=8), sc.Helix(grid_position=(0, 1), max_offset=8), - sc.Helix(grid_position=(0, 2), max_offset=8)] - design = sc.Design(helices=helices, grid=sc.honeycomb) - design.draw_strand(0, 0).to(8).cross(1).move(-8).cross(2).to(8) - - # expected values for verification - expected_num_nucleotides = 8 * 3 - expected_strand_length = 8 * 3 - - dat, top = design.to_oxdna_format() - dat = dat.strip().split('\n') - top = top.strip().split('\n') - - # check length of output files are as expected (matches # of nucleotides plus header size) - self.assertEqual(expected_num_nucleotides + 3, len(dat)) - self.assertEqual(expected_num_nucleotides + 1, len(top)) - - # find relevant values for nucleotides - cm_poss = [] # center of mass position - nbrs_3p = [] - nbrs_5p = [] - - for line in dat[3:]: - data = line.strip().split() - # make sure there are 15 values per line (3 values per vector * 5 vectors per line) - # order of vectors: center of mass position, backbone base, normal, velocity, angular velocity - self.assertEqual(15, len(data)) - - cm_poss.append(tuple([float(x) for x in data[0:3]])) - bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector - nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector - - # make sure normal vectors and backbone vectors are unit length - sqr_bb_vec = sum([x ** 2 for x in bb_vec]) - sqr_nm_vec = sum([x ** 2 for x in nm_vec]) - self.assertAlmostEqual(1.0, sqr_bb_vec) - self.assertAlmostEqual(1.0, sqr_nm_vec) - - for value in data[9:]: # values for velocity and angular velocity vectors are 0 - self.assertAlmostEqual(0, float(value)) - - strand1_idxs = [] - for nuc_idx, line in enumerate(top[1:]): - data = line.strip().split() - # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor - self.assertEqual(4, len(data)) - - # make sure there's only 1 strand - strand_num = int(data[0]) - self.assertEqual(1, strand_num) - # make sure base is valid - base = data[1] - self.assertIn(base, ['A', 'C', 'G', 'T']) - - nbrs_3p.append(int(data[2])) - nbrs_5p.append(int(data[3])) - - # append start of strand (no 5' neighbor) to list of indexes for strand - neighbor_5 = int(data[3]) - if neighbor_5 == -1: - strand1_start = nuc_idx - strand1_idxs.append(strand1_start) - - # reconstruct strand using indices from oxDNA files - next_idx = nbrs_3p[strand1_start] - while next_idx >= 0: - strand1_idxs.append(next_idx) - next_idx = nbrs_3p[strand1_idxs[-1]] - - # assert that strand is correct length - self.assertEqual(expected_strand_length, len(strand1_idxs)) - - for i in range(expected_strand_length - 1): - # ignore nucleotide distances between domains (on crossovers) - if i == 7: - continue - - if i == 15: - continue - - strand1_nuc_idx1 = strand1_idxs[i] - strand1_nuc_idx2 = strand1_idxs[i + 1] - - # find the center of mass for adjacent nucleotides - s1_cmp1 = cm_poss[strand1_nuc_idx1] - s1_cmp2 = cm_poss[strand1_nuc_idx2] - - # calculate and verify squared distance between adjacent nucleotides in a domain - diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) - sqr_dist1 = sum([x ** 2 for x in diff1]) - - self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) - - def test_deletion_design(self) -> None: - """ - 0 7 - [---X--> - """ - helix = [sc.Helix(max_offset=7)] - design = sc.Design(helices=helix, grid=sc.square) - design.draw_strand(0, 0).to(7) - design.add_deletion(helix=0, offset=4) - - # expected values for verification - expected_num_nucleotides = 6 - expected_strand_length = 6 - - dat, top = design.to_oxdna_format() - dat = dat.strip().split('\n') - top = top.strip().split('\n') - - # check length of output files are as expected (matches # of nucleotides plus header size) - self.assertEqual(expected_num_nucleotides + 3, len(dat)) - self.assertEqual(expected_num_nucleotides + 1, len(top)) - - # find relevant values for nucleotides - cm_poss = [] # center of mass position - nbrs_3p = [] - nbrs_5p = [] - - for line in dat[3:]: - data = line.strip().split() - # make sure there are 15 values per line (3 values per vector * 5 vectors per line) - # order of vectors: center of mass position, backbone base, normal, velocity, angular velocity - self.assertEqual(15, len(data)) - - cm_poss.append(tuple([float(x) for x in data[0:3]])) - bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector - nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector - - # make sure normal vectors and backbone vectors are unit length - sqr_bb_vec = sum([x ** 2 for x in bb_vec]) - sqr_nm_vec = sum([x ** 2 for x in nm_vec]) - self.assertAlmostEqual(1.0, sqr_bb_vec) - self.assertAlmostEqual(1.0, sqr_nm_vec) - - for value in data[9:]: # values for velocity and angular velocity vectors are 0 - self.assertAlmostEqual(0, float(value)) - - strand1_idxs = [] - for nuc_idx, line in enumerate(top[1:]): - data = line.strip().split() - # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor - self.assertEqual(4, len(data)) - - # make sure there's only 1 strand - strand_num = int(data[0]) - self.assertEqual(1, strand_num) - # make sure base is valid - base = data[1] - self.assertIn(base, ['A', 'C', 'G', 'T']) - - nbrs_3p.append(int(data[2])) - nbrs_5p.append(int(data[3])) - - # append start of strand (no 5' neighbor) to list of indexes for strand - neighbor_5 = int(data[3]) - if neighbor_5 == -1: - strand1_start = nuc_idx - strand1_idxs.append(strand1_start) - - # reconstruct strand using indices from oxDNA files - next_idx = nbrs_3p[strand1_start] - while next_idx >= 0: - strand1_idxs.append(next_idx) - next_idx = nbrs_3p[strand1_idxs[-1]] - - # assert that strand is correct length - self.assertEqual(expected_strand_length, len(strand1_idxs)) - - for i in range(expected_strand_length - 1): - strand1_nuc_idx1 = strand1_idxs[i] - strand1_nuc_idx2 = strand1_idxs[i + 1] - - # find the center of mass for adjacent nucleotides - s1_cmp1 = cm_poss[strand1_nuc_idx1] - s1_cmp2 = cm_poss[strand1_nuc_idx2] - - # calculate and verify squared distance between adjacent nucleotides in a domain - diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) - sqr_dist1 = sum([x ** 2 for x in diff1]) - - self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) - - def test_insertion_design(self) -> None: - """ - 0 7 - ^ insertion of length 1 - [------> - """ - helix = [sc.Helix(max_offset=10)] - design = sc.Design(helices=helix, grid=sc.square) - design.draw_strand(0, 0).to(7) - design.add_insertion(helix=0, offset=4, length=1) - - # expected values for verification - expected_num_nucleotides = 8 - expected_strand_length = 8 - - dat, top = design.to_oxdna_format() - dat = dat.strip().split('\n') - top = top.strip().split('\n') - - # check length of output files are as expected (matches # of nucleotides plus header size) - self.assertEqual(expected_num_nucleotides + 3, len(dat)) - self.assertEqual(expected_num_nucleotides + 1, len(top)) - - # find relevant values for nucleotides - cm_poss = [] # center of mass position - nbrs_3p = [] - nbrs_5p = [] - - for line in dat[3:]: - data = line.strip().split() - # make sure there are 15 values per line (3 values per vector * 5 vectors per line) - # order of vectors: center of mass position, backbone base, normal, velocity, angular velocity - self.assertEqual(15, len(data)) - - cm_poss.append(tuple([float(x) for x in data[0:3]])) - bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector - nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector - - # make sure normal vectors and backbone vectors are unit length - sqr_bb_vec = sum([x ** 2 for x in bb_vec]) - sqr_nm_vec = sum([x ** 2 for x in nm_vec]) - self.assertAlmostEqual(1.0, sqr_bb_vec) - self.assertAlmostEqual(1.0, sqr_nm_vec) - - for value in data[9:]: # values for velocity and angular velocity vectors are 0 - self.assertAlmostEqual(0, float(value)) - - strand1_idxs = [] - for nuc_idx, line in enumerate(top[1:]): - data = line.strip().split() - # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor - self.assertEqual(4, len(data)) - - # make sure there's only 1 strand - strand_num = int(data[0]) - self.assertEqual(1, strand_num) - # make sure base is valid - base = data[1] - self.assertIn(base, ['A', 'C', 'G', 'T']) - - nbrs_3p.append(int(data[2])) - nbrs_5p.append(int(data[3])) - - # append start of strand (no 5' neighbor) to list of indexes for strand - neighbor_5 = int(data[3]) - if neighbor_5 == -1: - strand1_start = nuc_idx - strand1_idxs.append(strand1_start) - - # reconstruct strand using indices from oxDNA files - next_idx = nbrs_3p[strand1_start] - while next_idx >= 0: - strand1_idxs.append(next_idx) - next_idx = nbrs_3p[strand1_idxs[-1]] - - # assert that strand is correct length - self.assertEqual(expected_strand_length, len(strand1_idxs)) - - for i in range(expected_strand_length - 1): - strand1_nuc_idx1 = strand1_idxs[i] - strand1_nuc_idx2 = strand1_idxs[i + 1] - - # find the center of mass for adjacent nucleotides - s1_cmp1 = cm_poss[strand1_nuc_idx1] - s1_cmp2 = cm_poss[strand1_nuc_idx2] - - # calculate and verify squared distance between adjacent nucleotides in a domain - diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) - sqr_dist1 = sum([x ** 2 for x in diff1]) - - self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) - - def test_loopout_design(self) -> None: - """ 2 strands, one with a loopout - 0 7 - ^ loopout at 4 of length = 4 bases - [------> - <------] - """ - helix = [sc.Helix(max_offset=14)] - design = sc.Design(helices=helix, grid=sc.square) - design.draw_strand(0, 0).to(4).loopout(0, 4).to(7) - design.draw_strand(0, 7).to(0) - - # expected values for verification - expected_num_nucleotides = 7 * 2 + 4 - expected_strand_1_length = 7 + 4 # strand 1 has loopout of 4 - expected_strand_2_length = 7 - - dat, top = design.to_oxdna_format() - dat = dat.strip().split('\n') - top = top.strip().split('\n') - - # check length of output files are as expected (matches # of nucleotides plus header size) - self.assertEqual(expected_num_nucleotides + 3, len(dat)) - self.assertEqual(expected_num_nucleotides + 1, len(top)) - - # find relevant values for nucleotides - cm_poss = [] # center of mass position - nbrs_3p = [] - nbrs_5p = [] - - for line in dat[3:]: - data = line.strip().split() - # make sure there are 15 values per line (3 values per vector * 5 vectors per line) - # order of vectors: center of mass position, backbone base, normal, velocity, angular velocity - self.assertEqual(15, len(data)) - - cm_poss.append(tuple([float(x) for x in data[0:3]])) - bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector - nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector - - # make sure normal vectors and backbone vectors are unit length - sqr_bb_vec = sum([x ** 2 for x in bb_vec]) - sqr_nm_vec = sum([x ** 2 for x in nm_vec]) - self.assertAlmostEqual(1.0, sqr_bb_vec) - self.assertAlmostEqual(1.0, sqr_nm_vec) - - for value in data[9:]: # values for velocity and angular velocity vectors are 0 - self.assertAlmostEqual(0, float(value)) - - strand1_idxs = [] - strand2_idxs = [] - for nuc_idx, line in enumerate(top[1:]): - data = line.strip().split() - # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor - self.assertEqual(4, len(data)) - - # make sure there are only 2 strands - strand_num = int(data[0]) - self.assertIn(strand_num, [1, 2]) - # make sure base is valid - base = data[1] - self.assertIn(base, ['A', 'C', 'G', 'T']) - - nbrs_3p.append(int(data[2])) - nbrs_5p.append(int(data[3])) - - # append start of strand (no 5' neighbor) to list of indexes for strand - neighbor_5 = int(data[3]) - if neighbor_5 == -1: - if strand_num == 1: - strand1_start = nuc_idx - strand1_idxs.append(strand1_start) - else: - strand2_start = nuc_idx - strand2_idxs.append(strand2_start) - - # reconstruct strands using indices from oxDNA files - next_idx = nbrs_3p[strand1_start] - while next_idx >= 0: - strand1_idxs.append(next_idx) - next_idx = nbrs_3p[strand1_idxs[-1]] - - next_idx = nbrs_3p[strand2_start] - while next_idx >= 0: - strand2_idxs.append(next_idx) - next_idx = nbrs_3p[strand2_idxs[-1]] - - # assert that strands are the correct length - self.assertEqual(expected_strand_1_length, len(strand1_idxs)) - self.assertEqual(expected_strand_2_length, len(strand2_idxs)) - - # calculate distance between nucleotides preceding and following loopout and confirm that it matches expected distance between nucleotides. - # loopout here is nucleotides 4 to 7, so check distance between nucleotides 3 to 8 - cm_poss_pre_loopout = cm_poss[strand1_idxs[3]] - cm_poss_post_loopout = cm_poss[strand1_idxs[8]] - diff = tuple([cm_poss_pre_loopout[j] - cm_poss_post_loopout[j] for j in range(3)]) - sqr_dist = sum([x ** 2 for x in diff]) - self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist) - - for i in range(expected_strand_1_length - 1): - - if i in [3, 4, 5, 6, - 7]: # skip nucleotide distances having to do with loopout, as these won't have regular distance between nucleotides (i = 3 denotes distance from 3 to 4, which includes loopout) - continue - - strand1_nuc_idx1 = strand1_idxs[i] - strand1_nuc_idx2 = strand1_idxs[i + 1] - - # find the center of mass for adjacent nucleotides - s1_cmp1 = cm_poss[strand1_nuc_idx1] - s1_cmp2 = cm_poss[strand1_nuc_idx2] - - # calculate and verify squared distance between adjacent nucleotides in a domain - diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) - sqr_dist1 = sum([x ** 2 for x in diff1]) - - self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) - - for i in range(expected_strand_2_length - 1): # check adjacent nucleotide distances for second strand - strand2_nuc_idx1 = strand2_idxs[i] - strand2_nuc_idx2 = strand2_idxs[i + 1] - - # find the center of mass for adjacent nucleotides - s2_cmp1 = cm_poss[strand2_nuc_idx1] - s2_cmp2 = cm_poss[strand2_nuc_idx2] - - # calculate and verify squared distance between adjacent nucleotides in a domain - diff2 = tuple([s2_cmp1[j] - s2_cmp2[j] for j in range(3)]) - sqr_dist2 = sum([x ** 2 for x in diff2]) - - self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist2) - - def test_file_output(self) -> None: - # Arbitrary design - helix = [sc.Helix(max_offset=14)] - design = sc.Design(helices=helix, grid=sc.square) - design.draw_strand(0, 0).to(4).loopout(0, 4).to(7) - design.draw_strand(0, 7).to(0) - - scriptname = os.path.basename(sys.argv[0])[:-3] - - dat, top = design.to_oxdna_format() - - with tempfile.TemporaryDirectory() as tmpdir: - # First, write to the directory, in which case the names should be the script name - design.write_oxdna_files(directory=tmpdir) - self.assertEqual(top, open(tmpdir + '/' + scriptname + '.top').read()) - self.assertEqual(dat, open(tmpdir + '/' + scriptname + '.dat').read()) - - # Now, write, to a specific filename without extensions - design.write_oxdna_files(directory=tmpdir, - filename_no_extension='oxdna-Export with spaces in name') - self.assertEqual(top, open(tmpdir + '/oxdna-Export with spaces in name.top').read()) - self.assertEqual(dat, open(tmpdir + '/oxdna-Export with spaces in name.dat').read()) - - -class TestPlateMaps(unittest.TestCase): - - def setUp(self) -> None: - helices = [sc.Helix(max_offset=100)] - self.design = sc.Design(helices=helices, strands=[], grid=sc.square) - self.design.draw_strand(0, 0).move(10).with_name('strand 0').with_idt(plate='plate 1', well='A1') - self.design.draw_strand(0, 10).move(10).with_name('strand 1').with_idt(plate='plate 1', well='A2') - self.design.draw_strand(0, 20).move(10).with_name('strand 2').with_idt(plate='plate 1', well='B2') - self.design.draw_strand(0, 30).move(10).with_name('strand 3').with_idt(plate='plate 1', well='B3') - self.design.draw_strand(0, 40).move(10).with_name('strand 4').with_idt(plate='plate 1', well='D7') - - def test_plate_map_markdown(self) -> None: - plate_maps = self.design.plate_maps() - self.assertEqual(1, len(plate_maps)) - plate_map = plate_maps[0] - actual_md = plate_map.to_table().strip() - expected_md = """ -### plate "plate 1" -| | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -|:----|:---------|:---------|:---------|:----|:----|:----|:---------|:----|:----|:-----|:-----|:-----| -| A | strand 0 | strand 1 | | | | | | | | | | | -| B | | strand 2 | strand 3 | | | | | | | | | | -| C | | | | | | | | | | | | | -| D | | | | | | | strand 4 | | | | | | -| E | | | | | | | | | | | | | -| F | | | | | | | | | | | | | -| G | | | | | | | | | | | | | -| H | | | | | | | | | | | | | -""".strip() - self.assertEqual(expected_md, actual_md) - - -class TestExtension(unittest.TestCase): - def test_to_json_serializable__extension_key_contains_num_bases(self) -> None: - ext = sc.Extension(5) - result = ext.to_json_serializable(False) - self.assertEqual(result[sc.extension_key], 5) - - def test_to_json_serializable__no_display_length_key_when_default_display_length(self) -> None: - ext = sc.Extension(5) - result = ext.to_json_serializable(False) - self.assertNotIn(sc.display_length_key, result) - - def test_to_json_serializable__no_display_angle_key_when_default_display_angle(self) -> None: - ext = sc.Extension(5) - result = ext.to_json_serializable(False) - self.assertNotIn(sc.display_angle_key, result) - - def test_to_json_serializable__no_name_key_when_default_name(self) -> None: - ext = sc.Extension(5) - result = ext.to_json_serializable(False) - self.assertNotIn(sc.domain_name_key, result) - - def test_to_json_serializable__no_label_key_when_default_label(self) -> None: - ext = sc.Extension(5) - result = ext.to_json_serializable(False) - self.assertNotIn(sc.domain_label_key, result) - - def test_to_json_serializable__display_length_key_contains_non_default_display_length(self) -> None: - ext = sc.Extension(5, display_length=1.9) - result = ext.to_json_serializable(False) - self.assertEqual(result[sc.display_length_key], 1.9) - - def test_to_json_serializable__display_angle_key_contains_non_default_display_angle(self) -> None: - ext = sc.Extension(5, display_angle=39.9) - result = ext.to_json_serializable(False) - self.assertEqual(result[sc.display_angle_key], 39.9) - - def test_to_json_serializable__name_key_contains_non_default_name(self) -> None: - ext = sc.Extension(5, name="A") - result = ext.to_json_serializable(False) - self.assertEqual(result[sc.domain_name_key], "A") - - def test_to_json_serializable__label_key_contains_non_default_name(self) -> None: - ext = sc.Extension(5, label="ext1") - result = ext.to_json_serializable(False) - self.assertEqual(result[sc.domain_label_key], "ext1") - - -class TestBasePairs(unittest.TestCase): - def setUp(self) -> None: - ''' - X shows position of mismatches - 111111111122222222223333333333 - 0123456789012345678901234567890123456789 - 0 [-->[--> [--> [--> [--> - <] <--------] <-] <-] <] <] - X - 111111111122222222223333333333 - 0123456789012345678901234567890123456789 - 1 [-----------> [--> - <--] <-----------] - X - ''' - helices = [sc.Helix(max_offset=40) for _ in range(2)] - self.design = sc.Design(helices=helices) - # helix 0 forward - self.design.draw_strand(0, 0).move(4).with_sequence('AAAA') - self.design.draw_strand(0, 4).move(4).with_sequence('AAAA') - self.design.draw_strand(0, 12).move(4).with_sequence('AAAA') - self.design.draw_strand(0, 20).move(4).with_sequence('AAAA') - self.design.draw_strand(0, 28).move(4).with_sequence('AAAA') - # helix 0 reverse - self.design.draw_strand(0, 3).move(-2).with_sequence('TT') - self.design.draw_strand(0, 14).to(4).with_sequence('TTTTTTTTCT') - self.design.draw_strand(0, 20).to(17).with_sequence('TTT') - self.design.draw_strand(0, 29).to(26).with_sequence('TTT') - self.design.draw_strand(0, 36).to(34).with_sequence('TT') - self.design.draw_strand(0, 39).to(37).with_sequence('TT') - # helix 1 forward - self.design.draw_strand(1, 4).to(17).with_sequence('A' * 13) - self.design.draw_strand(1, 20).to(24).with_sequence('A' * 4) - # helix 1 reverse - self.design.draw_strand(1, 12).to(8).with_sequence('TGTT') - self.design.draw_strand(1, 26).to(13).with_sequence('T' * 13) - - def test_find_overlapping_domains(self) -> None: - d01f = self.design.strands[0].domains[0] - d02f = self.design.strands[1].domains[0] - d03f = self.design.strands[2].domains[0] - d04f = self.design.strands[3].domains[0] - d05f = self.design.strands[4].domains[0] - - d01r = self.design.strands[5].domains[0] - d02r = self.design.strands[6].domains[0] - d03r = self.design.strands[7].domains[0] - d04r = self.design.strands[8].domains[0] - d05r = self.design.strands[9].domains[0] - d06r = self.design.strands[10].domains[0] - - d11f = self.design.strands[11].domains[0] - d12f = self.design.strands[12].domains[0] - - d11r = self.design.strands[13].domains[0] - d12r = self.design.strands[14].domains[0] - - overlapping_domains_h0 = sc.find_overlapping_domains_on_helix(self.design.helices[0]) - overlapping_domains_h1 = sc.find_overlapping_domains_on_helix(self.design.helices[1]) - - self.assertEqual(len(overlapping_domains_h0), 4) - self.assertEqual(len(overlapping_domains_h1), 3) - - self.assertIn((d01f, d01r), overlapping_domains_h0) - self.assertIn((d02f, d02r), overlapping_domains_h0) - self.assertIn((d03f, d02r), overlapping_domains_h0) - self.assertIn((d05f, d04r), overlapping_domains_h0) - - self.assertIn((d11f, d11r), overlapping_domains_h1) - self.assertIn((d11f, d12r), overlapping_domains_h1) - self.assertIn((d12f, d12r), overlapping_domains_h1) - ''' - 111111111122222222223333333333 - 0123456789012345678901234567890123456789 - 0 [-->[--> [--> [--> [--> - <] <--------] <-] <-] <] <] - - 111111111122222222223333333333 - 0123456789012345678901234567890123456789 - 1 [-----------> [--> - <--] <-----------] - ''' - - def test_design_base_pairs_mismatches(self) -> None: - base_pairs = self.design.base_pairs(allow_mismatches=True) - self.assertEqual(len(base_pairs), 2) - self.assertEqual(len(base_pairs[0]), 9) - self.assertEqual(len(base_pairs[1]), 12) - - # d01f, d01r - self.assertIn(1, base_pairs[0]) - self.assertIn(2, base_pairs[0]) - - # d02f, d02r - self.assertIn(4, base_pairs[0]) - self.assertIn(5, base_pairs[0]) - self.assertIn(6, base_pairs[0]) - self.assertIn(7, base_pairs[0]) - - # d03f, d02r - self.assertIn(12, base_pairs[0]) - self.assertIn(13, base_pairs[0]) - - # d05f, d04r - self.assertIn(28, base_pairs[0]) - - # d11f, d11r - self.assertIn(8, base_pairs[1]) - self.assertIn(9, base_pairs[1]) - self.assertIn(10, base_pairs[1]) - self.assertIn(11, base_pairs[1]) - - # d11f, d12r - self.assertIn(13, base_pairs[1]) - self.assertIn(14, base_pairs[1]) - self.assertIn(15, base_pairs[1]) - self.assertIn(16, base_pairs[1]) - - # d12f, d12r - self.assertIn(20, base_pairs[1]) - self.assertIn(21, base_pairs[1]) - self.assertIn(22, base_pairs[1]) - self.assertIn(23, base_pairs[1]) - ''' - 111111111122222222223333333333 - 0123456789012345678901234567890123456789 - 0 [-->[--> [--> [--> [--> - <] <--------] <-] <-] <] <] - - 111111111122222222223333333333 - 0123456789012345678901234567890123456789 - 1 [-----------> [--> - <--] <-----------] - ''' - - def test_design_base_pairs_no_mismatches(self) -> None: - base_pairs = self.design.base_pairs(allow_mismatches=False) - self.assertEqual(len(base_pairs), 2) - self.assertEqual(len(base_pairs[0]), 8) - self.assertEqual(len(base_pairs[1]), 11) - - # d01f, d01r - self.assertIn(1, base_pairs[0]) - self.assertIn(2, base_pairs[0]) - - # d02f, d02r - self.assertIn(4, base_pairs[0]) - # self.assertIn(5, base_pairs[0]) # mismatch - self.assertIn(6, base_pairs[0]) - self.assertIn(7, base_pairs[0]) - - # d03f, d02r - self.assertIn(12, base_pairs[0]) - self.assertIn(13, base_pairs[0]) - - # d05f, d04r - self.assertIn(28, base_pairs[0]) - - # d11f, d11r - self.assertIn(8, base_pairs[1]) - self.assertIn(9, base_pairs[1]) - # self.assertIn(10, base_pairs[1]) # mismatch - self.assertIn(11, base_pairs[1]) - - # d11f, d12r - self.assertIn(13, base_pairs[1]) - self.assertIn(14, base_pairs[1]) - self.assertIn(15, base_pairs[1]) - self.assertIn(16, base_pairs[1]) - - # d12f, d12r - self.assertIn(20, base_pairs[1]) - self.assertIn(21, base_pairs[1]) - self.assertIn(22, base_pairs[1]) - self.assertIn(23, base_pairs[1]) - ''' - X shows position of mismatches - 111111111122222222223333333333 - 0123456789012345678901234567890123456789 - 0 [-->[--> [--> [--> [--> - <] <--------] <-] <-] <] <] - X - 111111111122222222223333333333 - 0123456789012345678901234567890123456789 - 1 [-----------> [--> - <--] <-----------] - X - ''' - - def test_design_base_pairs_no_dna(self) -> None: - ''' - 0123456789 - 0 [--------> - <---]<---] - ''' - design = sc.Design(helices=[sc.Helix(max_offset=100)]) - design.draw_strand(0, 0).move(10) - design.draw_strand(0, 5).move(-5) - design.draw_strand(0, 10).move(-5) - - base_pairs = design.base_pairs() - self.assertEqual(len(base_pairs), 1) - self.assertEqual(len(base_pairs[0]), 10) - - for offset in range(10): - self.assertIn(offset, base_pairs[0]) - - def test_design_base_pairs_dna_on_some_strands_and_mismatches(self) -> None: - ''' - 0123456789 - AAAAAAAAAA - 0 [--------> - <---]<---] - TTCTT - ''' - design = sc.Design(helices=[sc.Helix(max_offset=100)]) - design.draw_strand(0, 0).move(10).with_sequence('A' * 10) - design.draw_strand(0, 5).move(-5).with_sequence('TTCTT') - design.draw_strand(0, 10).move(-5) - - base_pairs = design.base_pairs(allow_mismatches=False) - self.assertEqual(len(base_pairs), 1) - self.assertEqual(len(base_pairs[0]), 9) - - for offset in range(10): - if offset != 2: - self.assertIn(offset, base_pairs[0]) - - def test_design_base_pairs_deletions_insertions(self) -> None: - ''' - 0123456789 - AA - A AAAAAAA - 0 [XX---II-> - <-XX]<-II] - TT TTTTTT - TT - ''' - design = sc.Design(helices=[sc.Helix(max_offset=100)]) - design.draw_strand(0, 0).move(10).with_deletions([1, 2]).with_insertions([(6, 1), (7, 1)]) \ - .with_sequence('A' * 10) - design.draw_strand(0, 5).move(-5).with_deletions([2, 3]).with_sequence('TTT') - design.draw_strand(0, 10).move(-5).with_insertions([(7, 1), (8, 1)]).with_sequence('T' * 7) - - base_pairs = design.base_pairs(allow_mismatches=False) - self.assertEqual(len(base_pairs), 1) - self.assertEqual(len(base_pairs[0]), 5) - - self.assertIn(0, base_pairs[0]) - self.assertIn(4, base_pairs[0]) - self.assertIn(5, base_pairs[0]) - self.assertIn(7, base_pairs[0]) - self.assertIn(9, base_pairs[0]) - - def test_design_base_pairs_deletions_insertions_mismatch_in_insertion(self) -> None: - ''' - 0123456789 - AA - A AAAAAAA - 0 [XX---II-> - <-XX]<-II] - TT TTTTTT - CT - ''' - design = sc.Design(helices=[sc.Helix(max_offset=100)]) - design.draw_strand(0, 0).move(10).with_deletions([1, 2]).with_insertions([(6, 1), (7, 1)]) \ - .with_sequence('A' * 10) - design.draw_strand(0, 5).move(-5).with_deletions([2, 3]).with_sequence('TTT') - design.draw_strand(0, 10).move(-5).with_insertions([(7, 1), (8, 1)]).with_sequence('TTTCTTT') - - base_pairs = design.base_pairs(allow_mismatches=False) - self.assertEqual(len(base_pairs), 1) - self.assertEqual(len(base_pairs[0]), 4) - - self.assertIn(0, base_pairs[0]) - self.assertIn(4, base_pairs[0]) - self.assertIn(5, base_pairs[0]) - self.assertIn(9, base_pairs[0]) - - def test_no_base_pairs(self) -> None: - ''' - 0123456789 - [--> - <--] - ''' - design = sc.Design(helices=[sc.Helix(max_offset=100)]) - design.draw_strand(0, 0).move(4) - design.draw_strand(0, 9).move(-4) - - base_pairs = design.base_pairs(allow_mismatches=False) - self.assertEqual(len(base_pairs), 0) - - def test_no_base_pairs_only_forward_strand(self) -> None: - ''' - 0123456789 - [--> - ''' - design = sc.Design(helices=[sc.Helix(max_offset=100)]) - design.draw_strand(0, 0).move(4) - - base_pairs = design.base_pairs(allow_mismatches=False) - self.assertEqual(len(base_pairs), 0) +import dataclasses +import os +import sys +import tempfile +import unittest +import re +import json +import math +from typing import Iterable, Union, Dict, Any + +import xlrd # type: ignore + +import scadnano as sc +import scadnano.origami_rectangle as rect +import scadnano.modifications as mod + +from scadnano.scadnano import _convert_design_to_oxdna_system + + +def strand_matching(strands: Iterable[sc.Strand], helix: int, forward: bool, start: int, + end: int) -> sc.Strand: + """ + Finds strand whose first bound domain matches the given parameters. + """ + return next(s for s in strands if + s.first_bound_domain().helix == helix and + s.first_bound_domain().forward == forward and + s.first_bound_domain().start == start and + s.first_bound_domain().end == end) + + +def remove_whitespace(sequence: str) -> str: + sequence = re.sub(r'\s*', '', sequence) + return sequence + + +class TestCreateStrandChainedMethods(unittest.TestCase): + # tests methods for creating strands using chained method notation as in this issue: + # https://github.com/UC-Davis-molecular-computing/scadnano-python-package/issues/85 + + def setUp(self) -> None: + helices = [sc.Helix(max_offset=100) for _ in range(6)] + self.design_6helix: sc.Design = sc.Design(helices=helices, strands=[], grid=sc.square) + + def test_strand__loopouts_with_labels(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.loopout(1, 8) + sb.with_domain_label('loop0') + sb.to(5) + sb.with_domain_label('dom1') + sb.cross(2) + sb.to(10) + sb.with_domain_label('dom2') + sb.loopout(3, 12) + sb.with_domain_label('loop1') + sb.to(5) + expected_strand = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Loopout(8, label='loop0'), + sc.Domain(1, False, 5, 10, label='dom1'), + sc.Domain(2, True, 5, 10, label='dom2'), + sc.Loopout(12, label='loop1'), + sc.Domain(3, False, 5, 10), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + + def test_strand__loopouts_with_labels_and_colors_to_json(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.loopout(1, 8) + sb.with_domain_color(sc.Color(10, 10, 10)) + sb.with_domain_label('loop0') + sb.to(5) + sb.with_domain_label('dom1') + sb.with_domain_color(sc.Color(20, 20, 20)) + sb.cross(2) + sb.to(10) + sb.with_domain_label('dom2') + sb.loopout(3, 12) + sb.with_domain_label('loop1') + sb.to(5) + sb.with_color(sc.Color(30, 30, 30)) + design_json_map = design.to_json_serializable(suppress_indent=False) + design_from_json = sc.Design.from_scadnano_json_map(design_json_map) + expected_strand = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Loopout(8, label='loop0', color=sc.Color(10, 10, 10)), + sc.Domain(1, False, 5, 10, label='dom1', color=sc.Color(20, 20, 20)), + sc.Domain(2, True, 5, 10, label='dom2'), + sc.Loopout(12, label='loop1'), + sc.Domain(3, False, 5, 10), + ], color=sc.Color(30, 30, 30)) + self.assertEqual(1, len(design_from_json.strands)) + self.assertEqual(expected_strand, design_from_json.strands[0]) + self.assertEqual(expected_strand.color, sc.Color(30, 30, 30)) + + def test_strand__3p_extension(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + + sb.extension_3p(5) + sb.with_domain_color(sc.Color(10, 10, 10)) + + expected_strand: sc.Strand = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Extension(num_bases=5, color=sc.Color(10, 10, 10)), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + + def test_strand__5p_extension(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.extension_5p(5) + sb.to(10) + + expected_strand: sc.Strand = sc.Strand([ + sc.Extension(5), + sc.Domain(0, True, 0, 10), + ]) + + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + + def test_strand__update_to_after_5p_extension_ok(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.extension_5p(5) + + sb.to(10) + sb.update_to(15) + + expected_strand: sc.Strand = sc.Strand([ + sc.Extension(5), + sc.Domain(0, True, 0, 15), + ]) + + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + + def test_strand__move_after_5p_extension_ok(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.extension_5p(5) + + sb.move(15) + + expected_strand: sc.Strand = sc.Strand([ + sc.Extension(5), + sc.Domain(0, True, 0, 15), + ]) + + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + + def test_strand__to_after_3p_extension_should_raise_error(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.extension_3p(5) + + with self.assertRaises(sc.IllegalDesignError): + sb.to(15) + + def test_strand__move_after_3p_extension_should_raise_error(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.move(10) + sb.extension_3p(5) + + with self.assertRaises(sc.IllegalDesignError): + sb.move(5) + + def test_strand__cross_after_5p_extension_should_raise_error(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.extension_5p(5) + + with self.assertRaises(sc.IllegalDesignError): + sb.cross(1) + + def test_strand__cross_after_3p_extension_should_raise_error(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(5) + sb.extension_3p(5) + + with self.assertRaises(sc.IllegalDesignError): + sb.cross(1) + + def test_strand__extension_3p_after_loopout_should_raise_error(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.loopout(1, 3) + + with self.assertRaises(sc.IllegalDesignError): + sb.extension_3p(5) + + def test_strand__extension_3p_after_extension_should_raise_error(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.extension_3p(4) + + with self.assertRaises(sc.IllegalDesignError): + sb.extension_3p(5) + + def test_strand__update_to_after_3p_extension_should_raise_error(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.extension_3p(4) + + with self.assertRaises(sc.IllegalDesignError): + sb.update_to(15) + + def test_strand__as_circular_with_3p_extension_should_raise_error(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.extension_3p(4) + + with self.assertRaises(sc.IllegalDesignError): + sb.as_circular() + + def test_strand__as_circular_with_5p_extension_should_raise_error(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.extension_5p(4) + sb.to(10) + + with self.assertRaises(sc.IllegalDesignError): + sb.as_circular() + + def test_strand__extension_3p_on_circular_strand_should_raise_error(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.as_circular() + + with self.assertRaises(sc.IllegalDesignError): + sb.extension_3p(4) + + def test_strand__extension_3p_with_label(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.extension_3p(5) + sb.with_domain_label("ext1") + + expected_strand: sc.Strand = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Extension(5, label="ext1"), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + + def test_strand__extension_5p_with_label(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.extension_5p(5) + sb.with_domain_label("ext1") + sb.to(10) + + expected_strand: sc.Strand = sc.Strand([ + sc.Extension(5, label="ext1"), + sc.Domain(0, True, 0, 10) + ]) + + def test_strand__with_sequence_on_3p_extension(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.extension_3p(5) + sb.with_sequence("A" * 10 + "G" * 5) + + expected_strand: sc.Strand = sc.Strand([ + sc.Domain(0, True, 0, 10, dna_sequence="A" * 10), + sc.Extension(5, dna_sequence="G" * 5), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + + def test_strand__with_sequence_on_5p_extension(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.extension_5p(5) + sb.to(10) + sb.with_sequence("C" * 5 + "T" * 10) + + expected_strand: sc.Strand = sc.Strand([ + sc.Extension(5, dna_sequence="C" * 5), + sc.Domain(0, True, 0, 10, dna_sequence="T" * 10), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + + def test_strand__with_domain_sequence_on_extension(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.extension_3p(5) + sb.with_domain_sequence("G" * 5) + + expected_strand: sc.Strand = sc.Strand([ + sc.Domain(0, True, 0, 10, dna_sequence="?" * 10), + sc.Extension(5, dna_sequence="G" * 5), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + + def test_strand__extension_with_name(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.extension_3p(5) + sb.with_domain_name("ext1") + + expected_strand: sc.Strand = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Extension(5, name="ext1"), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + + def test_strand__with_relative_offset(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0).to(10) + + sb.extension_3p(5, display_length=1.4, display_angle=30) + + expected_strand: sc.Strand = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Extension(5, display_length=1.4, display_angle=30) + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + + def test_strand__0_0_to_10_cross_1_to_5(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.cross(1) + sb.to(5) + expected_strand = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Domain(1, False, 5, 10), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + self.assertEqual(1, len(design.helices[0].domains)) + self.assertEqual(1, len(design.helices[1].domains)) + self.assertEqual(0, len(design.helices[2].domains)) + self.assertEqual(0, len(design.helices[3].domains)) + self.assertEqual(0, len(design.helices[4].domains)) + self.assertEqual(0, len(design.helices[5].domains)) + + def test_strand__0_0_to_10_cross_1_to_5__reverse(self) -> None: + design = self.design_6helix + design.draw_strand(1, 5).to(10).cross(0).to(0) + expected_strand = sc.Strand([ + sc.Domain(1, True, 5, 10), + sc.Domain(0, False, 0, 10), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + self.assertEqual(1, len(design.helices[0].domains)) + self.assertEqual(1, len(design.helices[1].domains)) + self.assertEqual(0, len(design.helices[2].domains)) + self.assertEqual(0, len(design.helices[3].domains)) + self.assertEqual(0, len(design.helices[4].domains)) + self.assertEqual(0, len(design.helices[5].domains)) + + def test_strand__h0_off0_to_off10_cross_h1_to_off5_loopout_length3_h2_to_off15(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.cross(1) + sb.to(5) + sb.loopout(2, 3) + sb.to(15) + expected_strand = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Domain(1, False, 5, 10), + sc.Loopout(3), + sc.Domain(2, True, 5, 15), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + self.assertEqual(1, len(design.helices[0].domains)) + self.assertEqual(1, len(design.helices[1].domains)) + self.assertEqual(1, len(design.helices[2].domains)) + self.assertEqual(0, len(design.helices[3].domains)) + self.assertEqual(0, len(design.helices[4].domains)) + self.assertEqual(0, len(design.helices[5].domains)) + + def test_strand__two_forward_paranemic_crossovers(self) -> None: + design = self.design_6helix + design.draw_strand(0, 0).to(10).cross(1).to(15).cross(2).to(20) + expected_strand = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Domain(1, True, 10, 15), + sc.Domain(2, True, 15, 20), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + self.assertEqual(1, len(design.helices[0].domains)) + self.assertEqual(1, len(design.helices[1].domains)) + self.assertEqual(1, len(design.helices[2].domains)) + self.assertEqual(0, len(design.helices[3].domains)) + self.assertEqual(0, len(design.helices[4].domains)) + self.assertEqual(0, len(design.helices[5].domains)) + + def test_strand__two_reverse_paranemic_crossovers(self) -> None: + design = self.design_6helix + design.draw_strand(0, 20).to(10).cross(1).to(5).cross(2).to(0) + expected_strand = sc.Strand([ + sc.Domain(0, False, 10, 20), + sc.Domain(1, False, 5, 10), + sc.Domain(2, False, 0, 5), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + self.assertEqual(1, len(design.helices[0].domains)) + self.assertEqual(1, len(design.helices[1].domains)) + self.assertEqual(1, len(design.helices[2].domains)) + self.assertEqual(0, len(design.helices[3].domains)) + self.assertEqual(0, len(design.helices[4].domains)) + self.assertEqual(0, len(design.helices[5].domains)) + + def test_strand__multiple_strands(self) -> None: + design = self.design_6helix + design.draw_strand(0, 0).to(10).cross(1).to(0) + design.draw_strand(0, 20).to(10).cross(1).to(20) + expected_strand0 = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Domain(1, False, 0, 10), + ]) + expected_strand1 = sc.Strand([ + sc.Domain(0, False, 10, 20), + sc.Domain(1, True, 10, 20), + ]) + self.assertEqual(2, len(design.strands)) + self.assertEqual(expected_strand0, design.strands[0]) + self.assertEqual(expected_strand1, design.strands[1]) + self.assertEqual(2, len(design.helices[0].domains)) + self.assertEqual(2, len(design.helices[1].domains)) + self.assertEqual(0, len(design.helices[2].domains)) + self.assertEqual(0, len(design.helices[3].domains)) + self.assertEqual(0, len(design.helices[4].domains)) + self.assertEqual(0, len(design.helices[5].domains)) + + def test_strand__multiple_strands_other_order(self) -> None: + design = self.design_6helix + design.draw_strand(0, 20).to(10).cross(1).to(20) + design.draw_strand(0, 0).to(10).cross(1).to(0) + expected_strand0 = sc.Strand([ + sc.Domain(0, False, 10, 20), + sc.Domain(1, True, 10, 20), + ]) + expected_strand1 = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Domain(1, False, 0, 10), + ]) + self.assertEqual(2, len(design.strands)) + self.assertEqual(expected_strand0, design.strands[0]) + self.assertEqual(expected_strand1, design.strands[1]) + self.assertEqual(2, len(design.helices[0].domains)) + self.assertEqual(2, len(design.helices[1].domains)) + self.assertEqual(0, len(design.helices[2].domains)) + self.assertEqual(0, len(design.helices[3].domains)) + self.assertEqual(0, len(design.helices[4].domains)) + self.assertEqual(0, len(design.helices[5].domains)) + + def test_strand__multiple_strands_overlap_no_error(self) -> None: + design = self.design_6helix + design.draw_strand(0, 0).to(10).cross(1).to(0) \ + .as_scaffold() \ + .with_modification_internal(5, mod.cy3_int, warn_on_no_dna=False) + design.draw_strand(0, 10).to(0).cross(1).to(10).with_modification_5p(mod.biotin_5p) + expected_strand0 = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Domain(1, False, 0, 10), + ], is_scaffold=True) + expected_strand1 = sc.Strand([ + sc.Domain(0, False, 0, 10), + sc.Domain(1, True, 0, 10), + ]) + + expected_strand0.set_modification_internal(5, mod.cy3_int, warn_on_no_dna=False) + expected_strand1.set_modification_5p(mod.biotin_5p) + + self.assertEqual(2, len(design.strands)) + + self.assertEqual(expected_strand0, design.strands[0]) + self.assertEqual(None, design.strands[0].modification_5p) + self.assertEqual(None, design.strands[0].modification_3p) + self.assertDictEqual({5: mod.cy3_int}, design.strands[0].modifications_int) + + self.assertEqual(expected_strand1, design.strands[1]) + self.assertEqual(mod.biotin_5p, design.strands[1].modification_5p) + self.assertEqual(None, design.strands[1].modification_3p) + self.assertDictEqual({}, design.strands[1].modifications_int) + + self.assertEqual(2, len(design.helices[0].domains)) + self.assertEqual(2, len(design.helices[1].domains)) + self.assertEqual(0, len(design.helices[2].domains)) + self.assertEqual(0, len(design.helices[3].domains)) + self.assertEqual(0, len(design.helices[4].domains)) + self.assertEqual(0, len(design.helices[5].domains)) + + def test_strand__multiple_strands_overlap_error(self) -> None: + design = self.design_6helix + design.draw_strand(0, 0).to(10).cross(1).to(0) + with self.assertRaises(sc.IllegalDesignError): + design.draw_strand(0, 2).to(8) + + def test_strand__call_to_twice_legally(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.cross(1) + sb.to(5) + sb.to(0) + expected_strand = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Domain(1, False, 5, 10), + sc.Domain(1, False, 0, 5), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + self.assertEqual(1, len(design.helices[0].domains)) + self.assertEqual(2, len(design.helices[1].domains)) + self.assertEqual(0, len(design.helices[2].domains)) + self.assertEqual(0, len(design.helices[3].domains)) + self.assertEqual(0, len(design.helices[4].domains)) + self.assertEqual(0, len(design.helices[5].domains)) + + def test_strand__call_update_to_twice_legally(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.cross(1) + sb.update_to(5) + sb.update_to(0) + expected_strand = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Domain(1, False, 0, 10), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + self.assertEqual(1, len(design.helices[0].domains)) + self.assertEqual(1, len(design.helices[1].domains)) + self.assertEqual(0, len(design.helices[2].domains)) + self.assertEqual(0, len(design.helices[3].domains)) + self.assertEqual(0, len(design.helices[4].domains)) + self.assertEqual(0, len(design.helices[5].domains)) + + def test_strand__call_to_then_update_to_legally(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + sb.cross(1) + sb.to(5) + sb.update_to(0) + expected_strand = sc.Strand([ + sc.Domain(0, True, 0, 10), + sc.Domain(1, False, 0, 10), + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + self.assertEqual(1, len(design.helices[0].domains)) + self.assertEqual(1, len(design.helices[1].domains)) + self.assertEqual(0, len(design.helices[2].domains)) + self.assertEqual(0, len(design.helices[3].domains)) + self.assertEqual(0, len(design.helices[4].domains)) + self.assertEqual(0, len(design.helices[5].domains)) + + def test_strand__call_to_twice_increase_decrease_forward(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 0) + sb.to(10) + with self.assertRaises(sc.IllegalDesignError): + sb.to(5) + + def test_strand__call_to_twice_decrease_increase_reverse(self) -> None: + design = self.design_6helix + sb = design.draw_strand(0, 10) + sb.to(0) + with self.assertRaises(sc.IllegalDesignError): + sb.to(5) + + +class TestCreateHelix(unittest.TestCase): + + def test_helix_constructor_no_max_offset_with_major_ticks(self) -> None: + # tests bug where an exception is raised if major ticks is defined but not max_offset + sc.Helix(major_ticks=[0, 5, 10]) + + +class TestM13(unittest.TestCase): + + def test_p7249(self) -> None: + p7249 = sc.m13() + self.assertEqual('TTCCCTTCCTTTCTCG', p7249[:16]) + self.assertEqual(7249, len(p7249)) + p7249 = sc.m13(rotation=0) + self.assertEqual('AATGCTACTACTATTA', p7249[:16]) + self.assertEqual(7249, len(p7249)) + + def test_p7560(self) -> None: + p7560 = sc.m13(rotation=0, variant=sc.M13Variant.p7560) + self.assertEqual('AGCTTGGCACTGGCCG', p7560[:16]) + self.assertEqual(7560, len(p7560)) + + def test_p8064(self) -> None: + p8064 = sc.m13(rotation=0, variant=sc.M13Variant.p8064) + self.assertEqual('GGCAATGACCTGATAG', p8064[:16]) + self.assertEqual(8064, len(p8064)) + + +class TestModifications(unittest.TestCase): + + def test_to_json__names_unique_for_modifications_raises_no_error(self) -> None: + helices = [sc.Helix(max_offset=100)] + design: sc.Design = sc.Design(helices=helices, strands=[], grid=sc.square) + name = 'mod_name' + design.draw_strand(0, 0).move(5).with_modification_5p( + sc.Modification5Prime(display_text=name, id=name)) + design.draw_strand(0, 5).move(5).with_modification_3p( + sc.Modification3Prime(display_text=name, id=name + '3')) + design.to_json(True) + + def test_to_json__names_not_unique_for_modifications_raises_error(self) -> None: + helices = [sc.Helix(max_offset=100)] + design: sc.Design = sc.Design(helices=helices, strands=[], grid=sc.square) + name = 'mod_name' + design.draw_strand(0, 0).move(5).with_modification_5p( + sc.Modification5Prime(display_text=name, id=name)) + design.draw_strand(0, 5).move(5).with_modification_3p( + sc.Modification3Prime(display_text=name, id=name)) + with self.assertRaises(sc.IllegalDesignError): + design.to_json(True) + + def test_mod_illegal_exceptions_raised(self) -> None: + strand = sc.Strand(domains=[sc.Domain(0, True, 0, 5)], dna_sequence='AATGC') + strand.set_modification_internal(2, mod.biotin_int) + with self.assertRaises(sc.IllegalDesignError): + strand.set_modification_internal(1, mod.biotin_int) + + # biotin3_1 = mod.Biotin(location=sc.ModLocation.prime3) + # biotin3_2 = mod.Biotin(location=sc.ModLocation.prime3) + # with self.assertRaises(sc.IllegalDesignError): + # strand = sc.Strand(domains=[sc.Substrand(0, True, 0, 5)], dna_sequence='AATGC', + # modifications=[biotin3_1, biotin3_2]) + # + # biotin_i_1 = mod.Biotin(location=sc.ModLocation.internal, offset=2) + # biotin_i_2 = mod.Biotin(location=sc.ModLocation.internal, offset=2) + # with self.assertRaises(sc.IllegalDesignError): + # strand = sc.Strand(domains=[sc.Substrand(0, True, 0, 5)], dna_sequence='AATGC', + # modifications=[biotin_i_1, biotin_i_2]) + # + # biotin_i_small = mod.Biotin(location=sc.ModLocation.internal, offset=-1) + # with self.assertRaises(sc.IllegalDesignError): + # strand = sc.Strand(domains=[sc.Substrand(0, True, 0, 5)], dna_sequence='AATGC', + # modifications=[biotin_i_small]) + # seq = strand.idt_dna_sequence() + # + # biotin_i_large = mod.Biotin(location=sc.ModLocation.internal, offset=10) + # with self.assertRaises(sc.IllegalDesignError): + # strand = sc.Strand(domains=[sc.Substrand(0, True, 0, 5)], dna_sequence='AATGC', + # modifications=[biotin_i_small]) + # seq = strand.idt_dna_sequence() + # + # biotin_i_offset_not_T = mod.Biotin(location=sc.ModLocation.internal, offset=0) + # with self.assertRaises(sc.IllegalDesignError): + # strand = sc.Strand(domains=[sc.Substrand(0, True, 0, 5)], dna_sequence='AATGC', + # modifications=[biotin_i_offset_not_T]) + # seq = strand.idt_dna_sequence() + # + # cy3I_offset_off_end = mod.Cy3(location=sc.ModLocation.internal, offset=4) + # with self.assertRaises(sc.IllegalDesignError): + # strand = sc.Strand(domains=[sc.Substrand(0, True, 0, 5)], dna_sequence='AATGC', + # modifications=[cy3I_offset_off_end]) + # seq = strand.idt_dna_sequence() + + def test_Cy3(self) -> None: + cy3_5 = mod.cy3_5p + self.assertEqual(r'/5Cy3/', cy3_5.idt_text) + self.assertEqual(r'/5Cy3/', cy3_5.id) + self.assertEqual('Cy3', cy3_5.display_text) + cy3_3 = mod.cy3_3p + self.assertEqual(r'/3Cy3Sp/', cy3_3.idt_text) + self.assertEqual(r'/3Cy3Sp/', cy3_3.id) + self.assertEqual('Cy3', cy3_3.display_text) + # cy3_i1 = mod.Cy3(location=sc.ModLocation.internal, offset=1) + cy3_i1 = mod.cy3_int + self.assertEqual(r'/iCy3/', cy3_i1.idt_text) + self.assertEqual(r'/iCy3/', cy3_i1.id) + self.assertEqual('Cy3', cy3_i1.display_text) + # cy3_i2 = mod.Cy3(location=sc.ModLocation.internal, offset=3) + cy3_i2 = mod.cy3_int + self.assertEqual(r'/iCy3/', cy3_i2.idt_text) + self.assertEqual(r'/iCy3/', cy3_i2.id) + self.assertEqual('Cy3', cy3_i2.display_text) + + strand5 = sc.Strand(domains=[sc.Domain(0, True, 0, 5)], dna_sequence='ATTGC', + modification_5p=cy3_5) + strand3 = sc.Strand(domains=[sc.Domain(1, True, 0, 5)], dna_sequence='ATTGC', + modification_3p=cy3_3) + strand_i = sc.Strand(domains=[sc.Domain(2, True, 0, 5)], dna_sequence='ATTGC', + modifications_int={1: cy3_i1, 3: cy3_i2}) + strand53 = sc.Strand(domains=[sc.Domain(3, True, 0, 5)], dna_sequence='ATTGC', + modification_5p=cy3_5, modification_3p=cy3_3) + strand53_i = sc.Strand(domains=[sc.Domain(4, True, 0, 5)], dna_sequence='ATTGC', + modification_5p=cy3_5, modification_3p=cy3_3, + modifications_int={1: cy3_i1, 3: cy3_i2}) + + self.assertEqual(r'/5Cy3/ATTGC', strand5.idt_dna_sequence()) + self.assertEqual(r'ATTGC/3Cy3Sp/', strand3.idt_dna_sequence()) + self.assertEqual(r'/5Cy3/ATTGC/3Cy3Sp/', strand53.idt_dna_sequence()) + self.assertEqual(r'AT/iCy3/TG/iCy3/C', strand_i.idt_dna_sequence()) + self.assertEqual(r'/5Cy3/AT/iCy3/TG/iCy3/C/3Cy3Sp/', strand53_i.idt_dna_sequence()) + + def test_biotin(self) -> None: + biotin5 = mod.biotin_5p + self.assertEqual(r'/5Biosg/', biotin5.idt_text) + self.assertEqual(r'/5Biosg/', biotin5.id) + self.assertEqual('B', biotin5.display_text) + biotin3 = mod.biotin_3p + self.assertEqual(r'/3Bio/', biotin3.idt_text) + self.assertEqual(r'/3Bio/', biotin3.id) + self.assertEqual('B', biotin3.display_text) + # biotin_i_1 = mod.Biotin(location=sc.ModLocation.internal, offset=1) + biotin_i_1 = mod.biotin_int + self.assertEqual(r'/iBiodT/', biotin_i_1.idt_text) + self.assertEqual(r'/iBiodT/', biotin_i_1.id) + self.assertEqual('B', biotin_i_1.display_text) + # biotin_i_2 = mod.Biotin(location=sc.ModLocation.internal, offset=2) + biotin_i_2 = mod.biotin_int + self.assertEqual(r'/iBiodT/', biotin_i_2.idt_text) + self.assertEqual(r'/iBiodT/', biotin_i_2.id) + self.assertEqual('B', biotin_i_2.display_text) + + strand5 = sc.Strand(domains=[sc.Domain(0, True, 0, 5)], dna_sequence='ATTGC', + modification_5p=biotin5) + strand3 = sc.Strand(domains=[sc.Domain(1, True, 0, 5)], dna_sequence='ATTGC', + modification_3p=biotin3) + strand_i = sc.Strand(domains=[sc.Domain(2, True, 0, 5)], dna_sequence='ATTGC', + modifications_int={1: biotin_i_1, 2: biotin_i_2}) + strand53 = sc.Strand(domains=[sc.Domain(3, True, 0, 5)], dna_sequence='ATTGC', + modification_5p=biotin5, modification_3p=biotin3) + strand53i = sc.Strand(domains=[sc.Domain(4, True, 0, 5)], dna_sequence='ATTGC', + modification_5p=biotin5, modification_3p=biotin3, + modifications_int={1: biotin_i_1, 2: biotin_i_2}) + self.assertEqual(r'/5Biosg/ATTGC', strand5.idt_dna_sequence()) + self.assertEqual(r'ATTGC/3Bio/', strand3.idt_dna_sequence()) + self.assertEqual(r'A/iBiodT//iBiodT/GC', strand_i.idt_dna_sequence()) + self.assertEqual(r'/5Biosg/ATTGC/3Bio/', strand53.idt_dna_sequence()) + self.assertEqual(r'/5Biosg/A/iBiodT//iBiodT/GC/3Bio/', strand53i.idt_dna_sequence()) + + def test_to_json_serializable(self) -> None: + biotin5 = mod.biotin_5p + biotin5 = dataclasses.replace(biotin5, connector_length=6) + self.assertEqual(r'/5Biosg/', biotin5.idt_text) + self.assertEqual(r'/5Biosg/', biotin5.id) + self.assertEqual('B', biotin5.display_text) + self.assertEqual(6, biotin5.connector_length) + biotin3 = mod.biotin_3p + self.assertEqual(r'/3Bio/', biotin3.idt_text) + self.assertEqual(r'/3Bio/', biotin3.id) + self.assertEqual('B', biotin3.display_text) + # biotin_i_1 = mod.Biotin(location=sc.ModLocation.internal, offset=1) + biotin_i_1 = mod.biotin_int + self.assertEqual(r'/iBiodT/', biotin_i_1.idt_text) + self.assertEqual(r'/iBiodT/', biotin_i_1.id) + self.assertEqual('B', biotin_i_1.display_text) + # biotin_i_2 = mod.Biotin(location=sc.ModLocation.internal, offset=2) + biotin_i_2 = mod.biotin_int + self.assertEqual(r'/iBiodT/', biotin_i_2.idt_text) + self.assertEqual(r'/iBiodT/', biotin_i_2.id) + self.assertEqual('B', biotin_i_2.display_text) + + strand5 = sc.Strand(domains=[sc.Domain(0, True, 0, 5)], dna_sequence='ATTGC', + modification_5p=biotin5) + strand3 = sc.Strand(domains=[sc.Domain(1, True, 0, 5)], dna_sequence='ATTGC', + modification_3p=biotin3) + strand_i = sc.Strand(domains=[sc.Domain(2, True, 0, 5)], dna_sequence='ATTGC', + modifications_int={1: biotin_i_1, 2: biotin_i_2}) + strand53 = sc.Strand(domains=[sc.Domain(3, True, 0, 5)], dna_sequence='ATTGC', + modification_5p=biotin5, modification_3p=biotin3) + strand53_i = sc.Strand(domains=[sc.Domain(4, True, 0, 5)], dna_sequence='ATTGC', + modification_5p=biotin5, modification_3p=biotin3, + modifications_int={1: biotin_i_1, 2: biotin_i_2}) + + strands = [strand5, strand3, strand_i, strand53, strand53_i] + design = sc.Design(strands=strands, grid=sc.square) + + # print(design.to_json()) + + json_dict = design.to_json_serializable(suppress_indent=False) + self.assertTrue(sc.design_modifications_key in json_dict) + mods_dict = json_dict[sc.design_modifications_key] + self.assertTrue(r'/5Biosg/' in mods_dict) + self.assertTrue(r'/3Bio/' in mods_dict) + self.assertTrue(r'/iBiodT/' in mods_dict) + + biotin5_json = mods_dict[r'/5Biosg/'] + self.assertEqual('/5Biosg/', biotin5_json[sc.mod_idt_text_key]) + self.assertEqual('B', biotin5_json[sc.mod_display_text_key]) + self.assertEqual(6, biotin5_json[sc.mod_connector_length_key]) + + biotin3_json = mods_dict[r'/3Bio/'] + self.assertEqual('/3Bio/', biotin3_json[sc.mod_idt_text_key]) + self.assertEqual('B', biotin3_json[sc.mod_display_text_key]) + self.assertNotIn(sc.mod_connector_length_key, biotin3_json) + + strand5_mod5_json = json_dict[sc.strands_key][0][sc.modification_5p_key] + strand3_mod3_json = json_dict[sc.strands_key][1][sc.modification_3p_key] + self.assertEqual("/5Biosg/", strand5_mod5_json) + self.assertEqual("/3Bio/", strand3_mod3_json) + + strand_i_mods_int_json = json_dict[sc.strands_key][2][sc.modifications_int_key] + self.assertDictEqual({"1": "/iBiodT/", "2": "/iBiodT/"}, strand_i_mods_int_json) + + strand53_mod5_json = json_dict[sc.strands_key][3][sc.modification_5p_key] + strand53_mod3_json = json_dict[sc.strands_key][3][sc.modification_3p_key] + self.assertEqual("/5Biosg/", strand53_mod5_json) + self.assertEqual("/3Bio/", strand53_mod3_json) + + strand53_i_mod5_json = json_dict[sc.strands_key][4][sc.modification_5p_key] + strand53_i_mod3_json = json_dict[sc.strands_key][4][sc.modification_3p_key] + strand53_i_mods_int_json = json_dict[sc.strands_key][4][sc.modifications_int_key] + self.assertEqual("/5Biosg/", strand53_i_mod5_json) + self.assertEqual("/3Bio/", strand53_i_mod3_json) + self.assertDictEqual({"1": "/iBiodT/", "2": "/iBiodT/"}, strand53_i_mods_int_json) + + +class TestImportCadnanoV2(unittest.TestCase): + """ + Tests the import feature to cadnano v2 (see misc/cadnano-format-specs/v2.txt). + """ + folder = "cadnano_v2_import" + input_path = os.path.join('tests_inputs', folder) + # This variable is used for writing out files, which can be useful for debugging + output_path = os.path.join('tests_outputs', folder) + + def test_32_helix_rectangle(self) -> None: + design = sc.Design.from_cadnano_v2(directory=self.input_path, + filename='test_32_helix_rectangle.json') + self.assertEqual(35, len(design.helices)) + # To help with debugging, uncomment these lines to write out the + # scadnano file + # + design.write_scadnano_file(directory=self.output_path, + filename=f'test_32_helix_rectangle.{sc.default_scadnano_file_extension}') + + def test_helices_order(self) -> None: + design = sc.Design.from_cadnano_v2(directory=self.input_path, + filename='test_helices_order.json') + self.assertEqual(11, len(design.helices)) + # To help with debugging, uncomment these lines to write out the + # scadnano file + # + design.write_scadnano_file(directory=self.output_path, + filename=f'test_helices_order.{sc.default_scadnano_file_extension}') + + def test_helices_order2(self) -> None: + design = sc.Design.from_cadnano_v2(directory=self.input_path, + filename='test_helices_order2.json') + self.assertEqual(35, len(design.helices)) + # To help with debugging, uncomment these lines to write out the + # scadnano file + # + design.write_scadnano_file(directory=self.output_path, + filename=f'test_helices_order2.{sc.default_scadnano_file_extension}') + + def test_huge_hex(self) -> None: + design = sc.Design.from_cadnano_v2(directory=self.input_path, + filename='test_huge_hex.json') + self.assertEqual(26, len(design.helices)) + # To help with debugging, uncomment these lines to write out the + # scadnano file + # + design.write_scadnano_file(directory=self.output_path, + filename=f'test_huge_hex.{sc.default_scadnano_file_extension}') + + def test_Science09_prot120_98_v3(self) -> None: + file_name = "test_Science09_prot120_98_v3" + design = sc.Design.from_cadnano_v2(directory=self.input_path, + filename=file_name + ".json") + self.assertEqual(19, len(design.helices)) + # To help with debugging, uncomment these lines to write out the + # scadnano file + # + # design.write_scadnano_file(directory=self.output_path, + # filename=f'{file_name}.{sc.default_scadnano_file_extension}') + + def test_Nature09_monolith(self) -> None: + file_name = "test_Nature09_monolith" + design = sc.Design.from_cadnano_v2(directory=self.input_path, + filename=file_name + ".json") + self.assertEqual(60, len(design.helices)) + # Verify snippets of the design + self.assertEqual(4, len(design.strands_starting_on_helix(53))) + self.assertEqual(5, len(design.strands_starting_on_helix(42))) + # To help with debugging, uncomment these lines to write out the + # scadnano file + # + design.write_scadnano_file(directory=self.output_path, + filename=f'{file_name}.{sc.default_scadnano_file_extension}') + + def test_circular_auto_staple(self) -> None: + file_name = "test_circular_auto_staple" + design = sc.Design.from_cadnano_v2(directory=self.input_path, + filename=file_name + ".json") + self.assertEqual(8, len(design.helices)) + # To help with debugging, uncomment these lines to write out the + # scadnano file + # + design.write_scadnano_file(directory=self.output_path, + filename=f'{file_name}.{sc.default_scadnano_file_extension}') + + def test_circular_auto_staple_hex(self) -> None: + file_name = "test_circular_auto_staple_hex" + design = sc.Design.from_cadnano_v2(directory=self.input_path, + filename=file_name + ".json") + self.assertEqual(6, len(design.helices)) + # To help with debugging, uncomment these lines to write out the + # scadnano file + # + design.write_scadnano_file(directory=self.output_path, + filename=f'{file_name}.{sc.default_scadnano_file_extension}') + + def test_paranemic_crossover(self) -> None: + file_name = "test_crossover_to_same_helix" + design = sc.Design.from_cadnano_v2(directory=self.input_path, + filename=file_name + ".json") + self.assertEqual(1, len(design.helices)) + # To help with debugging, uncomment these lines to write out the + # scadnano file + # + design.write_scadnano_file(directory=self.output_path, + filename=f'{file_name}.{sc.default_scadnano_file_extension}') + + def test_same_helix_crossover(self) -> None: + file_name = "test_paranemic_crossover" + design = sc.Design.from_cadnano_v2(directory=self.input_path, + filename=file_name + ".json") + self.assertEqual(4, len(design.helices)) + + def test_2_stape_2_helix_origami_deletions_insertions(self) -> None: + file_name = "test_2_stape_2_helix_origami_deletions_insertions" + design = sc.Design.from_cadnano_v2(directory=self.input_path, + filename=file_name + ".json") + self.assertEqual(2, len(design.helices)) + self.assertEqual(design.grid, sc.Grid.square) + self.assertEqual(2, len(design.helices)) + output_helix_0 = design.helices[0] + output_helix_1 = design.helices[1] + self.assertEqual(output_helix_0.grid_position, (0, 0)) + self.assertEqual(output_helix_1.grid_position, (0, 1)) + self.assertEqual(3, len(design.strands)) + + # left staple + stap_left_ss1 = sc.Domain(helix=1, forward=True, start=0, end=16, deletions=[12], insertions=[(6, 3)]) + stap_left_ss0 = sc.Domain(helix=0, forward=False, start=0, end=16, deletions=[11, 12], + insertions=[(6, 1)]) + stap_left = sc.Strand(domains=[stap_left_ss1, stap_left_ss0]) + self.assertIn(stap_left, design.strands) + + # right staple + stap_right_ss0 = sc.Domain(helix=0, forward=False, start=16, end=32, deletions=[24], + insertions=[(18, 2)]) + stap_right_ss1 = sc.Domain(helix=1, forward=True, start=16, end=32, deletions=[24], + insertions=[(18, 4)]) + stap_right = sc.Strand(domains=[stap_right_ss0, stap_right_ss1]) + self.assertIn(stap_right, design.strands) + + # scaffold + scaf_ss1_left = sc.Domain(helix=1, forward=False, start=0, end=16, deletions=[12], + insertions=[(6, 3)]) + scaf_ss0 = sc.Domain(helix=0, forward=True, start=0, end=32, deletions=[11, 12, 24], + insertions=[(6, 1), (18, 2)]) + # loopout = sc.Loopout(length=3) No loopout in cadnano + scaf_ss1_right = sc.Domain(helix=1, forward=False, start=16, end=32, deletions=[24], + insertions=[(18, 4)]) + scaf = sc.Strand(domains=[scaf_ss1_left, scaf_ss0, scaf_ss1_right], is_scaffold=True) + self.assertIn(scaf, design.strands) + + # To help with debugging, uncomment these lines to write out the + # scadnano file + # + design.write_scadnano_file(directory=self.output_path, + filename=f'{file_name}.{sc.default_scadnano_file_extension}') + + +class TestExportDNASequences(unittest.TestCase): + + def setUp(self) -> None: + r""" Removing scaffold from this design: + 0 8 16 24 32 40 48 56 64 72 80 88 96 +0 +------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------+ + /<------- -------+ +------- -------- -------] <------- -------- -------- -------] <------+ +------- -------]\ + ( | | | | ) +1 \[------- -------+ +------> [------+ +------- -------- -------> [------+ +------- -------+ +------- ------->/ + +------- -------- -------- -------- -------- -------+ +------- -------- -------- -------- -------- -------+ + | | | | | | +2 +------- -------- -------- -------- -------- -------+ +------- -------- -------- -------- -------- -------+ + /<------- -------+ +------- -------+ +------] <------- -------- -------+ +------] <------+ +------- -------]\ + ( | | | | ) +3 \[------- -------+ +------> [------+ +------- -------- -------> [------+ +------- -------+ +------- ------->/ + +------- -------- -------- -------- -------- -------+ +------- -------- -------- -------- -------- -------+ + | | | | | | +4 +------- -------- -------- -------- -------- -------+ +------- -------- -------- -------- -------- -------+ + /<------- -------+ +------- -------+ +------] <------- -------- -------+ +------] <------+ +------- -------]\ + ( | | | | ) +5 \[------- -------+ +------> [------- -------- -------- -------> [------- -------- -------+ +------- ------->/ + +------- -------- -------- -------- -------- -------] <------- -------- -------- -------- -------- -------+ + +gives this (removed scaffold to make it easier for me to visually track the strands) + + 0 8 16 24 32 40 48 56 64 72 80 88 96 +0 <-------A-------+ +-------D--------D-------] <-------G--------G--------G-------] <------+ +-------P-------] + | | | | +1 [-------A-------+ +------> [------+ +-------H--------H-------> [------+ +-------M-------+ +-------P-------> + | | | | +2 <-------B-------+ +-------E-------+ +------] <-------I--------I-------+ +------] <------+ +-------Q-------] + | | | | +3 [-------B-------+ +------> [------+ +-------J--------J-------> [------+ +-------N-------+ +-------Q-------> + | | | | +4 <-------C-------+ +-------F-------+ +------] <-------K--------K-------+ +------] <------+ +-------R-------] + | | | | +5 [-------C-------+ +------> [-------L--------L--------L-------> [-------O--------O-------+ +-------R-------> + +We give the strands simple single-letter names to help test they appear in the correct order in the output. + +row major 5': DGPAEIHMQBFKJNRCLO +col major 5': ABCEFLDHJIKOGMNPQR +row major 3': AGMDHPBINEJQCKOFLR +col major 3': ABCDEFGIKHJLMNOPQR +row major 5' or 3': ADGMPEHIBNQFJKCORL +col major 5' or 3': ABCDEFLHJGIKOMNPQR +row major top-left domain start: ADGMPEHIBNQFJKCORL +col major top-left domain start: ABCDEFLHJGIKMNOPQR + """ + helices = [sc.Helix(max_offset=100) for _ in range(6)] + self.design_6h: sc.Design = sc.Design(helices=helices, strands=[], grid=sc.square) + d = self.design_6h + + d.draw_strand(1, 0).move(16).cross(0).move(-16).with_name('A') + d.draw_strand(3, 0).move(16).cross(2).move(-16).with_name('B') + d.draw_strand(5, 0).move(16).cross(4).move(-16).with_name('C') + + d.draw_strand(0, 40).move(-24).cross(1).move(8).with_name('D') + + d.draw_strand(1, 24).move(8).cross(2).move(-16).cross(3).move(8).with_name('E') + d.draw_strand(3, 24).move(8).cross(4).move(-16).cross(5).move(8).with_name('F') + + d.draw_strand(0, 72).move(-32).with_name('G') + + d.draw_strand(2, 40).move(-8).cross(1).move(24).with_name('H') + d.draw_strand(1, 56).move(8).cross(2).move(-24).with_name('I') + + d.draw_strand(4, 40).move(-8).cross(3).move(24).with_name('J') + d.draw_strand(3, 56).move(8).cross(4).move(-24).with_name('K') + + d.draw_strand(5, 24).move(32).with_name('L') + + d.draw_strand(2, 72).move(-8).cross(1).move(16).cross(0).move(-8).with_name('M') + d.draw_strand(4, 72).move(-8).cross(3).move(16).cross(2).move(-8).with_name('N') + + d.draw_strand(5, 56).move(24).cross(4).move(-8).with_name('O') + + d.draw_strand(0, 96).move(-16).cross(1).move(16).with_name('P') + d.draw_strand(2, 96).move(-16).cross(3).move(16).with_name('Q') + d.draw_strand(4, 96).move(-16).cross(5).move(16).with_name('R') + + for strand in d.strands: + d.assign_dna(strand, 'A' * 32, assign_complement=False) + + @staticmethod + def _get_names_idt(design: sc.Design, key: sc.KeyFunction[sc.Strand]) -> str: + # call design.to_idt_bulk_input_format with given key functions, + # get IDT names of strands exported, and return them joined into a single string + idt_str = design.to_idt_bulk_input_format(key=key) + idt_lines = idt_str.split('\n') + names = [] + for line in idt_lines: + name = line.split(',')[0] + names.append(name) + names_joined = ''.join(names) + return names_joined + + def test_to_idt_bulk_input_format__row_major_5p(self) -> None: + key = sc.strand_order_key_function(column_major=False, strand_order=sc.StrandOrder.five_prime) + names_joined = self._get_names_idt(self.design_6h, key) + self.assertEqual('DGPAEIHMQBFKJNRCLO', names_joined) + + def test_to_idt_bulk_input_format__col_major_5p(self) -> None: + key = sc.strand_order_key_function(column_major=True, strand_order=sc.StrandOrder.five_prime) + names_joined = self._get_names_idt(self.design_6h, key) + self.assertEqual('ABCEFLDHJIKOGMNPQR', names_joined) + + def test_to_idt_bulk_input_format__row_major_3p(self) -> None: + key = sc.strand_order_key_function(column_major=False, strand_order=sc.StrandOrder.three_prime) + names_joined = self._get_names_idt(self.design_6h, key) + self.assertEqual('AGMDHPBINEJQCKOFLR', names_joined) + + def test_to_idt_bulk_input_format__col_major_3p(self) -> None: + key = sc.strand_order_key_function(column_major=True, strand_order=sc.StrandOrder.three_prime) + names_joined = self._get_names_idt(self.design_6h, key) + self.assertEqual('ABCDEFGIKHJLMNOPQR', names_joined) + + def test_to_idt_bulk_input_format__row_major_5p_or_3p(self) -> None: + key = sc.strand_order_key_function(column_major=False, + strand_order=sc.StrandOrder.five_or_three_prime) + names_joined = self._get_names_idt(self.design_6h, key) + self.assertEqual('ADGMPEHIBNQFJKCORL', names_joined) + + def test_to_idt_bulk_input_format__col_major_5p_or_3p(self) -> None: + key = sc.strand_order_key_function(column_major=True, strand_order=sc.StrandOrder.five_or_three_prime) + names_joined = self._get_names_idt(self.design_6h, key) + self.assertEqual('ABCDEFLHJGIKOMNPQR', names_joined) + + def test_to_idt_bulk_input_format__row_major_top_left_domain_start(self) -> None: + key = sc.strand_order_key_function(column_major=False, strand_order=sc.StrandOrder.top_left_domain) + names_joined = self._get_names_idt(self.design_6h, key) + self.assertEqual('ADGMPEHIBNQFJKCORL', names_joined) + + def test_to_idt_bulk_input_format__col_major_top_left_domain_start(self) -> None: + key = sc.strand_order_key_function(column_major=True, strand_order=sc.StrandOrder.top_left_domain) + names_joined = self._get_names_idt(self.design_6h, key) + self.assertEqual('ABCDEFLHJGIKMNOPQR', names_joined) + + def test_to_idt_bulk_input_format__duplicate_names_same_sequence(self) -> None: + length = 8 + helices = [sc.Helix(max_offset=length)] + ss1_r = sc.Domain(0, True, 0, 4) + ss2_r = sc.Domain(0, True, 4, 8) + ss_l = sc.Domain(0, False, 0, 4) + + s1_r = sc.Strand([ss1_r], idt=sc.IDTFields(), name='s1_r') + s2_r = sc.Strand([ss2_r], idt=sc.IDTFields(), name='s1_r') + s_l = sc.Strand([ss_l], idt=sc.IDTFields(), name='s_l') + + strands = [s1_r, s2_r, s_l] + + design = sc.Design(helices=helices, strands=strands, grid=sc.square) + + design.assign_dna(s_l, 'AGTT') + design.assign_dna(s2_r, 'AACT') + + # should not raise exception + design.to_idt_bulk_input_format() + + def test_to_idt_bulk_input_format__duplicate_names_different_sequences(self) -> None: + ss1_r = sc.Domain(0, True, 0, 4) + ss2_r = sc.Domain(0, True, 4, 8) + ss_l = sc.Domain(0, False, 0, 4) + + s1_r = sc.Strand([ss1_r], idt=sc.IDTFields(), name='s1_r') + s2_r = sc.Strand([ss2_r], idt=sc.IDTFields(), name='s1_r') + s_l = sc.Strand([ss_l], idt=sc.IDTFields(), name='s_l') + + strands = [s1_r, s2_r, s_l] + + design = sc.Design(strands=strands, grid=sc.square) + + design.assign_dna(s_l, 'AGTT') + design.assign_dna(s2_r, 'GGGG') + + with self.assertRaises(sc.IllegalDesignError): + design.to_idt_bulk_input_format() + + def test_to_idt_bulk_input_format__duplicate_names_different_scales(self) -> None: + ss1_r = sc.Domain(0, True, 0, 4) + ss2_r = sc.Domain(0, True, 4, 8) + ss_l = sc.Domain(0, False, 0, 4) + + s1_r = sc.Strand([ss1_r], idt=sc.IDTFields(scale='25nm'), name='s1_r') + s2_r = sc.Strand([ss2_r], idt=sc.IDTFields(scale='100nm'), name='s1_r') + s_l = sc.Strand([ss_l], idt=sc.IDTFields(), name='s_l') + + strands = [s1_r, s2_r, s_l] + + design = sc.Design(strands=strands, grid=sc.square) + + design.assign_dna(s_l, 'AGTT') + design.assign_dna(s2_r, 'AACT') + + with self.assertRaises(sc.IllegalDesignError): + design.to_idt_bulk_input_format() + + # https://stackoverflow.com/a/46307456/5339430 + # @mock.patch('sys.stdout', new_callable=io.StringIO) + def test_to_idt_bulk_input_format__duplicate_names_different_purifications(self) -> None: + ss1_r = sc.Domain(0, True, 0, 4) + ss2_r = sc.Domain(0, True, 4, 8) + ss_l = sc.Domain(0, False, 0, 4) + + s1_r = sc.Strand([ss1_r], idt=sc.IDTFields(purification='STD'), name='s1_r') + s2_r = sc.Strand([ss2_r], idt=sc.IDTFields(purification='HPLC'), name='s1_r') + s_l = sc.Strand([ss_l], idt=sc.IDTFields(), name='s_l') + + strands = [s1_r, s2_r, s_l] + + design = sc.Design(strands=strands, grid=sc.square) + + design.assign_dna(s_l, 'AGTT') + design.assign_dna(s2_r, 'AACT') + + with self.assertRaises(sc.IllegalDesignError): + design.to_idt_bulk_input_format(warn_duplicate_name=True) + # printed = mock_print.getvalue() + # self.assertIn('two strands with same IDT name', printed) + # self.assertIn('s1_r', printed) + + def test_write_idt_plate_excel_file(self) -> None: + strand_len = 10 + + # add 10 strands in excess of 3 plates + for plate_type in [sc.PlateType.wells96, sc.PlateType.wells384]: + num_strands = 3 * plate_type.num_wells_per_plate() + 10 + filename = f'test_excel_export_{plate_type.num_wells_per_plate()}.xls' + max_offset = num_strands * strand_len + helices = [sc.Helix(max_offset=max_offset) for _ in range(1)] + design = sc.Design(helices=helices, strands=[], grid=sc.square) + for strand_idx in range(num_strands): + design.draw_strand(0, strand_len * strand_idx).move(strand_len).with_name(f's{strand_idx}') + design.strands[-1].set_dna_sequence('T' * strand_len) + + design.write_idt_plate_excel_file(filename=filename, plate_type=plate_type) + + book = xlrd.open_workbook(filename) + self.assertEqual(4, book.nsheets) + for plate in range(4): + sheet = book.sheet_by_index(plate) + self.assertEqual(3, sheet.ncols) + + if plate == 2: # penultimate plate + expected_wells = plate_type.num_wells_per_plate() - plate_type.min_wells_per_plate() + 10 + elif plate == 3: # last plate + expected_wells = plate_type.min_wells_per_plate() + else: + expected_wells = plate_type.num_wells_per_plate() + + self.assertEqual(expected_wells + 1, sheet.nrows) + + os.remove(filename) + + +class TestExportCadnanoV2(unittest.TestCase): + """ + Tests the export feature to cadnano v2 (see misc/cadnano-format-specs/v2.txt). + """ + folder = "cadnano_v2_export" + input_path = os.path.join('tests_inputs', folder) + # This variable is used for writing out files, which can be useful for debugging + output_path = os.path.join('tests_outputs', folder) + ext = sc.default_scadnano_file_extension + + def test_export_design_with_helix_group(self): + e = 'east' + s = 'south' + helices = [ + sc.Helix(max_offset=24, group=s), + sc.Helix(max_offset=25, group=s), + ] + helices.extend([ + sc.Helix(max_offset=22, group=e), + sc.Helix(max_offset=23, group=e), + ]) + + group_south = sc.HelixGroup(position=sc.Position3D(x=0, y=10, z=0), + grid=sc.square) + group_east = sc.HelixGroup(position=sc.Position3D(x=10, y=0, z=0), grid=sc.square) + + groups = { + e: group_east, + s: group_south + } + + design = sc.Design(helices=helices, groups=groups, strands=[]) + output_json = design.to_cadnano_v2_json() + + output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) + self.assertEqual(4, len(output_design.helices)) + + # To help with debugging, uncomment these lines to write out the + # scadnano and/or cadnano file + # + # design.write_scadnano_file(directory=self.input_path, + # filename=f'test_export_design_with_helix_group.{self.ext}') + # design.write_cadnano_v2_file(directory=self.output_path, + # filename='test_export_design_with_helix_group.json') + + def test_export_design_with_helix_group_not_same_grid(self): + e = 'east' + s = 'south' + helices = [ + sc.Helix(max_offset=24, group=s), + sc.Helix(max_offset=25, group=s), + ] + helices.extend([ + sc.Helix(max_offset=22, group=e), + sc.Helix(max_offset=23, group=e), + ]) + + group_south = sc.HelixGroup(position=sc.Position3D(x=0, y=10, z=0), + grid=sc.square) + group_east = sc.HelixGroup(position=sc.Position3D(x=10, y=0, z=0), grid=sc.honeycomb) + + groups = { + e: group_east, + s: group_south + } + + design = sc.Design(helices=helices, groups=groups, strands=[]) + + # To help with debugging, uncomment these lines to write out the + # scadnano file + # + # design.write_scadnano_file(directory=self.input_path, + # filename=f'test_export_design_with_helix_group_not_same_grid.{self.ext}') + + with self.assertRaises(ValueError) as context: + design.to_cadnano_v2_json() + # design.write_cadnano_v2_file(directory=self.output_path, + # filename='test_export_design_with_helix_group_not_same_grid.json') + self.assertTrue('helix groups' in context.exception.args[0]) + + def test_2_staple_2_helix_origami_extremely_simple(self): + helices = [sc.Helix(max_offset=32), sc.Helix(max_offset=32)] + scaf_part = sc.Domain(helix=0, forward=True, start=0, end=32) + scaf = sc.Strand(domains=[scaf_part], is_scaffold=True) + design = sc.Design(helices=helices, strands=[scaf], grid=sc.square) + output_json = design.to_cadnano_v2_json() + + output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) + self.assertEqual(2, len(output_design.helices)) + # To help with debugging, uncomment these lines to write out the + # scadnano and/or cadnano file + # + # design.write_scadnano_file(directory=self.input_path, + # filename=f'test_2_stape_2_helix_origami_extremely_simple.{self.ext}') + # design.write_cadnano_v2_file(directory=self.output_path, + # filename='test_2_stape_2_helix_origami_extremely_simple.json') + + def test_2_staple_2_helix_origami_extremely_simple_2(self) -> None: + helices = [sc.Helix(max_offset=32), sc.Helix(max_offset=32)] + scaf_part1 = sc.Domain(helix=0, forward=True, start=0, end=32) + scaf_part2 = sc.Domain(helix=1, forward=False, start=0, end=32) + scaf = sc.Strand(domains=[scaf_part1, scaf_part2], is_scaffold=True) + design = sc.Design(helices=helices, strands=[scaf], grid=sc.square) + + # To help with debugging, uncomment these lines to write out the + # scadnano and/or cadnano file + # + # design.write_scadnano_file(directory=self.input_path, + # filename=f'test_2_stape_2_helix_origami_extremely_simple_2.{self.ext}') + # design.write_cadnano_v2_file(directory=self.output_path, + # filename='test_2_stape_2_helix_origami_extremely_simple_2.json') + output_json = design.to_cadnano_v2_json() + + output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) + self.assertEqual(2, len(output_design.helices)) + + def test_2_staple_2_helix_origami_deletions_insertions(self) -> None: + # left staple + stap_left_ss1 = sc.Domain(helix=1, forward=True, start=0, end=16) + stap_left_ss0 = sc.Domain(helix=0, forward=False, start=0, end=16) + stap_left = sc.Strand(domains=[stap_left_ss1, stap_left_ss0]) + + # right staple + stap_right_ss0 = sc.Domain(helix=0, forward=False, start=16, end=32) + stap_right_ss1 = sc.Domain(helix=1, forward=True, start=16, end=32) + stap_right = sc.Strand(domains=[stap_right_ss0, stap_right_ss1]) + + # scaffold + scaf_ss1_left = sc.Domain(helix=1, forward=False, start=0, end=16) + scaf_ss0 = sc.Domain(helix=0, forward=True, start=0, end=32) + # loopout = sc.Loopout(length=3) No loopout in cadnano + scaf_ss1_right = sc.Domain(helix=1, forward=False, start=16, end=32) + scaf = sc.Strand(domains=[scaf_ss1_left, scaf_ss0, scaf_ss1_right], is_scaffold=True) + + # whole design + design = sc.Design(strands=[scaf, stap_left, stap_right], grid=sc.square) + + # deletions and insertions added to design so they can be added to both strands on a helix + design.add_deletion(helix=0, offset=11) + design.add_deletion(helix=0, offset=12) + design.add_deletion(helix=0, offset=24) + design.add_deletion(helix=1, offset=12) + design.add_deletion(helix=1, offset=24) + + design.add_insertion(helix=0, offset=6, length=1) + design.add_insertion(helix=0, offset=18, length=2) + design.add_insertion(helix=1, offset=6, length=3) + design.add_insertion(helix=1, offset=18, length=4) + + # also assigns complement to strands other than scaf bound to it + output_json = design.to_cadnano_v2_json() + + output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) + # To help with debugging, uncomment these lines to write out the + self.assertEqual(output_design.grid, sc.Grid.square) + self.assertEqual(2, len(output_design.helices)) + output_helix_0 = output_design.helices[0] + output_helix_1 = output_design.helices[1] + self.assertEqual(output_helix_0.grid_position, (0, 0)) + self.assertEqual(output_helix_1.grid_position, (0, 1)) + self.assertEqual(3, len(output_design.strands)) + self.assertIn(stap_left, output_design.strands) + self.assertIn(stap_right, output_design.strands) + self.assertIn(scaf, output_design.strands) + # scadnano and/or cadnano file + # + # design.write_scadnano_file(directory=self.input_path, + # filename=f'test_2_stape_2_helix_origami_deletions_insertions.{self.ext}') + # design.write_cadnano_v2_file(directory=self.output_path, + # filename='test_2_stape_2_helix_origami_deletions_insertions.json') + + def test_6_helix_origami_rectangle(self) -> None: + design = rect.create(num_helices=6, num_cols=10, nick_pattern=rect.staggered, + twist_correction_deletion_spacing=3) + output_json = design.to_cadnano_v2_json() + + output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) + # To help with debugging, uncomment these lines to write out the + self.assertEqual(6, len(output_design.helices)) + + # scadnano and/or cadnano file + # + # design.write_scadnano_file(directory=self.input_path, + # filename=f'test_6_helix_origami_rectangle.{self.ext}') + # design.write_cadnano_v2_file(directory=self.output_path, + # filename='test_6_helix_origami_rectangle.json') + + def test_export_no_whitespace(self) -> None: + design = rect.create(num_helices=6, num_cols=10, nick_pattern=rect.staggered, + twist_correction_deletion_spacing=3) + output_json_with_space = design.to_cadnano_v2_json(whitespace=True) + self.assertIn(' ', output_json_with_space) + self.assertIn('\n', output_json_with_space) + output_json_no_space = design.to_cadnano_v2_json(whitespace=False) + self.assertNotIn(' ', output_json_no_space) + self.assertNotIn('\n', output_json_no_space) + + # scadnano and/or cadnano file + # + # design.write_scadnano_file(directory=self.input_path, + # filename=f'test_6_helix_origami_rectangle.{self.ext}') + # design.write_cadnano_v2_file(directory=self.output_path, + # filename='test_6_helix_origami_rectangle.json') + + def test_6_helix_bundle_honeycomb(self) -> None: + design = sc.Design.from_scadnano_file( + os.path.join(self.input_path, f'test_6_helix_bundle_honeycomb.{self.ext}')) + output_json = design.to_cadnano_v2_json() + + output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) + # To help with debugging, uncomment these lines to write out the + self.assertEqual(6, len(output_design.helices)) + # cadnano file + # + # design.write_cadnano_v2_file(directory=self.output_path, + # filename='test_6_helix_bundle_honeycomb.json') + + def test_16_helix_origami_rectangle_no_twist(self) -> None: + design = rect.create(num_helices=16, num_cols=26, assign_seq=True, + twist_correction_deletion_spacing=3) + # To help with debugging, uncomment these lines to write out the + # scadnano and/or cadnano file + # + # design.write_scadnano_file(directory=self.input_path, + # filename=f'test_16_helix_origami_rectangle_no_twist.{self.ext}') + # design.write_cadnano_v2_file(directory=self.output_path, + # filename='test_16_helix_origami_rectangle_no_twist.json') + output_json = design.to_cadnano_v2_json() + + output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) + self.assertEqual(16, len(output_design.helices)) + + def test_circular_strand(self) -> None: + helices = [sc.Helix(max_offset=24) for _ in range(2)] + design = sc.Design(helices=helices, grid=sc.square) + + design.draw_strand(1, 0).move(8).cross(0).move(-8).as_circular() + # To help with debugging, uncomment these lines to write out the + # scadnano and/or cadnano file + # + # design.write_scadnano_file(directory=self.input_path, + # filename=f'test_circular_strand.{self.ext}') + # design.write_cadnano_v2_file(directory=self.output_path, + # filename='test_circular_strand.json') + output_json = design.to_cadnano_v2_json() + + output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) + self.assertEqual(2, len(output_design.helices)) + + def test_big_circular_staples_hex(self) -> None: + design = sc.Design.from_scadnano_file( + os.path.join(self.input_path, f'test_big_circular_staples_hex.{self.ext}')) + # To help with debugging, uncomment these lines to write out the + # scadnano and/or cadnano file + # + # design.write_cadnano_v2_file(directory=self.output_path, + # filename='test_big_circular_staples_hex.json') + output_json = design.to_cadnano_v2_json() + + output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) + self.assertEqual(6, len(output_design.helices)) + + def test_big_circular_staples(self) -> None: + design = sc.Design.from_scadnano_file( + os.path.join(self.input_path, f'test_big_circular_staples.{self.ext}')) + # To help with debugging, uncomment these lines to write out the + # scadnano and/or cadnano file + # + # design.write_cadnano_v2_file(directory=self.output_path, + # filename='test_big_circular_staples.json') + output_json = design.to_cadnano_v2_json() + + output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) + self.assertEqual(8, len(output_design.helices)) + + def test_paranemic_crossover(self) -> None: + design = sc.Design.from_scadnano_file( + os.path.join(self.input_path, f'test_paranemic_crossover.{self.ext}')) + # To help with debugging, uncomment these lines to write out the + # scadnano and/or cadnano file + # + # design.write_cadnano_v2_file(directory=self.output_path, + # filename='test_paranemic_crossover.json') + output_json = design.to_cadnano_v2_json() + + output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) + self.assertEqual(4, len(output_design.helices)) + + def test_parity_issue(self) -> None: + """ We do not design where the parity of the helix + does not correspond to the direction. + """ + # Bad case one: parity issue in design (see cadnano v2 format spec, v2.txt) + helices = [sc.Helix(max_offset=32), sc.Helix(max_offset=32)] + scaf_part = sc.Domain(helix=1, forward=True, start=0, end=32) + scaf = sc.Strand(domains=[scaf_part], is_scaffold=True) + design = sc.Design(helices=helices, strands=[scaf], grid=sc.square) + + with self.assertRaises(ValueError) as context: + # To help with debugging, uncomment these lines to write out the + # scadnano and/or cadnano file + # + # design.write_cadnano_v2_file(directory=self.output_path, + # filename='test_parity_issue.json') + design.to_cadnano_v2_json() + self.assertTrue('forward' in context.exception.args[0]) + + def test_loopout(self) -> None: + """ We do not handle Loopouts + """ + # Bad case two: Loopouts + helices = [sc.Helix(max_offset=48), sc.Helix(max_offset=48)] + + # left staple + stap_left_ss1 = sc.Domain(helix=1, forward=True, start=8, end=24) + stap_left_ss0 = sc.Domain(helix=0, forward=False, start=8, end=24) + stap_left = sc.Strand(domains=[stap_left_ss1, stap_left_ss0]) + + # right staple + stap_right_ss0 = sc.Domain(helix=0, forward=False, start=24, end=40) + stap_right_ss1 = sc.Domain(helix=1, forward=True, start=24, end=40) + stap_right = sc.Strand(domains=[stap_right_ss0, stap_right_ss1]) + + # scaffold + scaf_ss1_left = sc.Domain(helix=1, forward=False, start=8, end=24) + scaf_ss0 = sc.Domain(helix=0, forward=True, start=8, end=40) + loopout = sc.Loopout(length=3) + scaf_ss1_right = sc.Domain(helix=1, forward=False, start=24, end=40) + scaf = sc.Strand(domains=[scaf_ss1_left, scaf_ss0, loopout, scaf_ss1_right], is_scaffold=True) + + # whole design + design = sc.Design(helices=helices, strands=[scaf, stap_left, stap_right], grid=sc.square) + + # deletions and insertions added to design are added to both strands on a helix + design.add_deletion(helix=1, offset=20) + design.add_insertion(helix=0, offset=14, length=1) + design.add_insertion(helix=0, offset=26, length=2) + + with self.assertRaises(ValueError) as context: + # To help with debugging, uncomment these lines to write out the + # scadnano and/or cadnano file + # + # design.write_cadnano_v2_file(directory=self.output_path, + # filename='test_loopout_issue.json') + design.to_cadnano_v2_json() + self.assertTrue('Loopouts' in context.exception.args[0]) + + def test_extension(self) -> None: + """ We do not handle Extensions + """ + design: sc.Design = sc.Design(helices=[sc.Helix(max_offset=100)], grid=sc.Grid.square) + sb = design.draw_strand(0, 0) + sb.to(10) + sb.as_scaffold() + sb.extension_3p(5) + + with self.assertRaises(ValueError) as context: + design.to_cadnano_v2_json() + self.assertTrue('Extensions' in context.exception.args[0]) + + +class TestDesignFromJson(unittest.TestCase): + """ + Tests reading a design from a dict derived from JSON. + """ + + def setUp(self) -> None: + r""" + 0 8 16 + | | | + 0 +--X-----------+ + /<--X---++------]\ + | || loopout(3) + \[---2--++------>/ + 1 +---2--]<------+ + """ + st_l = sc.Strand([ + sc.Domain(1, True, 0, 8, insertions=[(4, 2)]), + sc.Domain(0, False, 0, 8, deletions=[3]), + ], modification_5p=mod.biotin_5p) + st_r = sc.Strand([ + sc.Domain(0, False, 8, 16), + sc.Domain(1, True, 8, 16), + ], modification_5p=mod.biotin_5p, modification_3p=mod.cy3_3p, modifications_int={ + 1: mod.biotin_int, 2: mod.cy5_int + }) + scaf = sc.Strand([ + sc.Domain(1, False, 0, 8, insertions=[(4, 2)]), + sc.Domain(0, True, 0, 16, deletions=[3]), + sc.Loopout(3), + sc.Domain(1, False, 8, 16, deletions=[]), + ], is_scaffold=True) + + self.design_pre_json = sc.Design(strands=[st_l, st_r, scaf], grid=sc.square) + self.design_pre_json.assign_dna(scaf, 'A' * 36) + + def test_from_json__from_and_to_file_contents(self) -> None: + json_str = self.design_pre_json.to_json() + json_map = json.loads(json_str) + design = sc.Design.from_scadnano_json_map(json_map) + design.to_json_serializable() + + def test_from_json__three_strands(self) -> None: + json_str = self.design_pre_json.to_json() + json_map = json.loads(json_str) + design = sc.Design.from_scadnano_json_map(json_map) + + self.assertTrue(isinstance(design, sc.Design)) + + self.assertEqual(sc.Grid.square, design.grid) + + self.assertEqual(2, len(design.helices)) + helix0 = design.helices[0] + helix1 = design.helices[1] + self.assertEqual(0, helix0.idx) + self.assertEqual(0, helix0.min_offset) + self.assertEqual(16, helix0.max_offset) + self.assertEqual((0, 0), helix0.grid_position) + self.assertEqual(1, helix1.idx) + self.assertEqual(0, helix1.min_offset) + self.assertEqual(16, helix1.max_offset) + self.assertEqual((0, 1), helix1.grid_position) + + self.assertEqual(3, len(design.strands)) + st_l = design.strands[0] + st_r = design.strands[1] + scaf = design.strands[2] + + self.assertEqual(scaf, design.scaffold) + + self.assertEqual(2, len(st_l.domains)) + self.assertEqual(2, len(st_r.domains)) + self.assertEqual(4, len(scaf.domains)) + + self.assertEqual('A' * 36, scaf.dna_sequence) + self.assertEqual('T' * 17, st_l.dna_sequence) + self.assertEqual('T' * 16, st_r.dna_sequence) + + st_l_ss0 = st_l.domains[0] + st_l_ss1 = st_l.domains[1] + st_r_ss0 = st_r.domains[0] + st_r_ss1 = st_r.domains[1] + scaf_ss0 = scaf.domains[0] + scaf_ss1 = scaf.domains[1] + scaf_loop = scaf.domains[2] + scaf_ss2 = scaf.domains[3] + + self.assertEqual(3, scaf_loop.length) + + self.assertEqual(1, st_l_ss0.helix) + self.assertEqual(0, st_l_ss1.helix) + self.assertEqual(0, st_r_ss0.helix) + self.assertEqual(1, st_r_ss1.helix) + self.assertEqual(1, scaf_ss0.helix) + self.assertEqual(0, scaf_ss1.helix) + self.assertEqual(1, scaf_ss2.helix) + + self.assertEqual(True, st_l_ss0.forward) + self.assertEqual(False, st_l_ss1.forward) + self.assertEqual(False, st_r_ss0.forward) + self.assertEqual(True, st_r_ss1.forward) + self.assertEqual(False, scaf_ss0.forward) + self.assertEqual(True, scaf_ss1.forward) + self.assertEqual(False, scaf_ss2.forward) + + self.assertEqual(0, st_l_ss0.start) + self.assertEqual(8, st_l_ss0.end) + self.assertEqual(0, st_l_ss1.start) + self.assertEqual(8, st_l_ss1.end) + self.assertEqual(8, st_r_ss0.start) + self.assertEqual(16, st_r_ss0.end) + self.assertEqual(8, st_r_ss1.start) + self.assertEqual(16, st_r_ss1.end) + self.assertEqual(0, scaf_ss0.start) + self.assertEqual(8, scaf_ss0.end) + self.assertEqual(0, scaf_ss1.start) + self.assertEqual(16, scaf_ss1.end) + self.assertEqual(8, scaf_ss2.start) + self.assertEqual(16, scaf_ss2.end) + + self.assertListEqual([(4, 2)], st_l_ss0.insertions) + self.assertListEqual([], st_l_ss0.deletions) + self.assertListEqual([], st_l_ss1.insertions) + self.assertListEqual([3], st_l_ss1.deletions) + self.assertListEqual([], st_r_ss0.insertions) + self.assertListEqual([], st_r_ss0.deletions) + self.assertListEqual([], st_r_ss1.insertions) + self.assertListEqual([], st_r_ss1.deletions) + self.assertListEqual([(4, 2)], scaf_ss0.insertions) + self.assertListEqual([], scaf_ss0.deletions) + self.assertListEqual([], scaf_ss1.insertions) + self.assertListEqual([3], scaf_ss1.deletions) + self.assertListEqual([], scaf_ss2.insertions) + self.assertListEqual([], scaf_ss2.deletions) + + self.assertEqual(mod.biotin_5p, st_l.modification_5p) + self.assertEqual(None, st_l.modification_3p) + self.assertDictEqual({}, st_l.modifications_int) + + self.assertEqual(mod.biotin_5p, st_r.modification_5p) + self.assertEqual(mod.cy3_3p, st_r.modification_3p) + self.assertDictEqual({1: mod.biotin_int, 2: mod.cy5_int}, st_r.modifications_int) + + self.assertEqual(None, scaf.modification_5p) + self.assertEqual(None, scaf.modification_3p) + self.assertDictEqual({}, scaf.modifications_int) + + def test_from_json__helices_non_default_indices(self) -> None: + h2 = sc.Helix(idx=2) + h3 = sc.Helix(idx=3) + h5 = sc.Helix(idx=5) + helices = [h2, h3, h5] + s1 = sc.Strand([ + sc.Domain(2, True, 0, 4), + sc.Domain(3, False, 0, 4), + ]) + s2 = sc.Strand([ + sc.Domain(3, True, 4, 8), + sc.Domain(5, False, 4, 8), + ]) + self.design_pre_json = sc.Design(helices=helices, strands=[s1, s2], grid=sc.square) + + json_str = self.design_pre_json.to_json() + json_map = json.loads(json_str) + design = sc.Design.from_scadnano_json_map(json_map) + + self.assertEqual(3, len(design.helices)) + + self.assertEqual(2, design.helices[2].idx) + self.assertEqual(3, design.helices[3].idx) + self.assertEqual(5, design.helices[5].idx) + + def test_from_json__helices_non_default_indices_mixed_with_default(self) -> None: + h2 = sc.Helix(idx=2) + h3 = sc.Helix() + h5 = sc.Helix(idx=5) + helices = [h2, h3, h5] + s1 = sc.Strand([ + sc.Domain(2, True, 0, 4), + sc.Domain(1, False, 0, 4), + ]) + s2 = sc.Strand([ + sc.Domain(1, True, 4, 8), + sc.Domain(5, False, 4, 8), + ]) + self.design_pre_json = sc.Design(helices=helices, strands=[s1, s2], grid=sc.square) + + json_str = self.design_pre_json.to_json() + + json_map = json.loads(json_str) + design = sc.Design.from_scadnano_json_map(json_map) + + self.assertEqual(3, len(design.helices)) + + self.assertEqual(2, design.helices[2].idx) + self.assertEqual(1, design.helices[1].idx) + self.assertEqual(5, design.helices[5].idx) + + def test_from_json__helices_non_default_error_if_some_have_idx_not_others(self) -> None: + h2 = sc.Helix(idx=2) + h3 = sc.Helix(idx=3) + h3_2 = sc.Helix(idx=3) + helices = [h2, h3, h3_2] + with self.assertRaises(sc.IllegalDesignError): + self.design_pre_json = sc.Design(helices=helices, strands=[], grid=sc.square) + + def test_from_json__helices_non_default_error_if_some_have_idx_not_others_mixed_default(self) -> None: + h0 = sc.Helix() + h0_2 = sc.Helix(idx=0) + helices = [h0, h0_2] + with self.assertRaises(sc.IllegalDesignError): + self.design_pre_json = sc.Design(helices=helices, strands=[], grid=sc.square) + + +class TestStrandReversePolarity(unittest.TestCase): + """ + Tests reversing polarity of all strands in the system. + """ + + def test_reverse_all__one_strand(self) -> None: + """ + before + 0 8 + | | + 0 [---X--> + + after + 0 <---X--] + """ + design = sc.Design( + strands=[sc.Strand([sc.Domain(0, True, 0, 8, deletions=[4])])], + grid=sc.square) + design.reverse_all() + + self.assertEqual(1, len(design.strands)) + strand = design.strands[0] + self.assertEqual(1, len(strand.domains)) + self.assertEqual(7, strand.offset_5p()) + self.assertEqual(0, strand.offset_3p()) + ss = strand.domains[0] + self.assertEqual(0, ss.helix) + self.assertEqual(False, ss.forward) + self.assertEqual(0, ss.start) + self.assertEqual(8, ss.end) + + def test_reverse_all__three_strands(self) -> None: + r""" + before + 0 8 16 + | | | + + 0 +--X-----------+ + /<--X---++------]\ + | || | + \[---2--++------>/ + 1 +---2--]<------+ + + after + 0 +--X-----------+ + /[--X---++------>\ + | || | + \<---2--++------]/ + 1 +---2-->[------+ + """ + design = sc.Design( + strands=[ + sc.Strand([ + sc.Domain(1, True, 0, 8, insertions=[(4, 2)]), + sc.Domain(0, False, 0, 8, deletions=[3]), + ]), + sc.Strand([ + sc.Domain(0, False, 8, 16), + sc.Domain(1, True, 8, 16), + ]), + sc.Strand([ + sc.Domain(1, False, 0, 8, insertions=[(4, 2)]), + sc.Domain(0, True, 0, 16, deletions=[3]), + sc.Domain(1, False, 8, 16, deletions=[]), + ]), + ], + grid=sc.square) + design.reverse_all() + + self.assertEqual(3, len(design.strands)) + stap_l = design.strands[0] + stap_r = design.strands[1] + scaf = design.strands[2] + + self.assertEqual(0, stap_l.offset_5p()) + self.assertEqual(0, stap_l.offset_3p()) + self.assertEqual(15, stap_r.offset_5p()) + self.assertEqual(15, stap_r.offset_3p()) + self.assertEqual(8, scaf.offset_5p()) + self.assertEqual(7, scaf.offset_3p()) + + self.assertEqual(2, len(stap_l.domains)) + stap_l_ss0 = stap_l.domains[0] + stap_l_ss1 = stap_l.domains[1] + + self.assertEqual(0, stap_l_ss0.helix) + self.assertEqual(True, stap_l_ss0.forward) + self.assertEqual(0, stap_l_ss0.start) + self.assertEqual(8, stap_l_ss0.end) + self.assertEqual([3], stap_l_ss0.deletions) + self.assertEqual([], stap_l_ss0.insertions) + + self.assertEqual(1, stap_l_ss1.helix) + self.assertEqual(False, stap_l_ss1.forward) + self.assertEqual(0, stap_l_ss1.start) + self.assertEqual(8, stap_l_ss1.end) + self.assertEqual([], stap_l_ss1.deletions) + self.assertEqual([(4, 2)], stap_l_ss1.insertions) + + self.assertEqual(2, len(stap_r.domains)) + stap_r_ss0 = stap_r.domains[0] + stap_r_ss1 = stap_r.domains[1] + + self.assertEqual(1, stap_r_ss0.helix) + self.assertEqual(False, stap_r_ss0.forward) + self.assertEqual(8, stap_r_ss0.start) + self.assertEqual(16, stap_r_ss0.end) + self.assertEqual([], stap_r_ss0.deletions) + self.assertEqual([], stap_r_ss0.insertions) + + self.assertEqual(0, stap_r_ss1.helix) + self.assertEqual(True, stap_r_ss1.forward) + self.assertEqual(8, stap_r_ss1.start) + self.assertEqual(16, stap_r_ss1.end) + self.assertEqual([], stap_r_ss1.deletions) + self.assertEqual([], stap_r_ss1.insertions) + + self.assertEqual(3, len(scaf.domains)) + scaf_ss0 = scaf.domains[0] + scaf_ss1 = scaf.domains[1] + scaf_ss2 = scaf.domains[2] + + self.assertEqual(1, scaf_ss0.helix) + self.assertEqual(True, scaf_ss0.forward) + self.assertEqual(8, scaf_ss0.start) + self.assertEqual(16, scaf_ss0.end) + self.assertEqual([], scaf_ss0.deletions) + self.assertEqual([], scaf_ss0.insertions) + + self.assertEqual(0, scaf_ss1.helix) + self.assertEqual(False, scaf_ss1.forward) + self.assertEqual(0, scaf_ss1.start) + self.assertEqual(16, scaf_ss1.end) + self.assertEqual([3], scaf_ss1.deletions) + self.assertEqual([], scaf_ss1.insertions) + + self.assertEqual(1, scaf_ss2.helix) + self.assertEqual(True, scaf_ss2.forward) + self.assertEqual(0, scaf_ss2.start) + self.assertEqual(8, scaf_ss2.end) + self.assertEqual([], scaf_ss2.deletions) + self.assertEqual([(4, 2)], scaf_ss2.insertions) + + +class TestInlineInsDel(unittest.TestCase): + """ + Tests inlining of insertions/deletions. + """ + + def setUp(self) -> None: + self.design = sc.Design( + helices=[sc.Helix(max_offset=24, major_tick_distance=8)], + strands=[], + grid=sc.square) + + def test_no_deletion_after_loopout(self) -> None: + # not really a test of inlining, but I added the with_deletions and with_insertions to help these + # tests, so easier just to test this behavior here + with self.assertRaises(ValueError): + self.design.draw_strand(0, 0).move(8).loopout(0, 5, 10).with_deletions(4) + + def test_no_insertion_after_loopout(self) -> None: + # not really a test of inlining, but I added the with_deletions and with_insertions to help these + # tests, so easier just to test this behavior here + with self.assertRaises(ValueError): + self.design.draw_strand(0, 0).move(8).loopout(0, 5, 10).with_insertions((4, 2)) + + def test_deletion_below_range(self) -> None: + with self.assertRaises(ValueError): + self.design.draw_strand(0, 4).move(4).with_deletions(2) + + def test_deletion_above_range(self) -> None: + with self.assertRaises(ValueError): + self.design.draw_strand(0, 0).move(4).with_deletions(6) + + def test_insertion_below_range(self) -> None: + with self.assertRaises(ValueError): + self.design.draw_strand(0, 4).move(4).with_insertions((2, 1)) + + def test_insertion_above_range(self) -> None: + with self.assertRaises(ValueError): + self.design.draw_strand(0, 0).move(4).with_insertions((6, 1)) + + def test_inline_deletions_insertions__one_deletion(self) -> None: + """ + before + 0 4 8 16 24 + | | | | + 0 [---X--> + + after + 0 4 7 15 23 + | | | | + 0 [-----> + """ + design = self.design + design.draw_strand(0, 0).move(8).with_deletions(4) + design.inline_deletions_insertions() + self.assert_helix0_strand0_inlined(design, max_offset=23, major_ticks=[0, 7, 15, 23], start=0, end=7) + + def assert_helix0_strand0_inlined(self, design, max_offset, major_ticks, start, end): + self.assertEqual(1, len(design.helices)) + self.assertEqual(1, len(design.strands)) + helix = design.helices[0] + strand = design.strands[0] + self.assertEqual(max_offset, helix.max_offset) + self.assertEqual(major_ticks, helix.major_ticks) + self.assertEqual(start, strand.domains[0].start) + self.assertEqual(end, strand.domains[0].end) + self.assertListEqual([], strand.domains[0].deletions) + self.assertListEqual([], strand.domains[0].insertions) + + def test_inline_deletions_insertions__two_deletions(self) -> None: + """ + before + 0 2 4 8 16 24 + | | | | + 0 [-X-X--> + + after + 0 6 14 22 + | | | | + 0 [----> + """ + design = self.design + design.draw_strand(0, 0).move(8).with_deletions([2, 4]) + design.inline_deletions_insertions() + self.assert_helix0_strand0_inlined(design, max_offset=22, major_ticks=[0, 6, 14, 22], start=0, end=6) + + def test_inline_deletions_insertions__one_insertion(self) -> None: + """ + before + 0 4 8 16 24 + | | | | + 0 [---1--> + + after + 0 9 17 25 + | | | | + 0 [-------> + """ + design = self.design + design.draw_strand(0, 0).move(8).with_insertions((4, 1)) + design.inline_deletions_insertions() + self.assert_helix0_strand0_inlined(design, max_offset=25, major_ticks=[0, 9, 17, 25], start=0, end=9) + + def test_inline_deletions_insertions__two_insertions(self) -> None: + """ + before + 0 2 4 8 16 24 + | | | | + 0 [-3-1--> + + after + 0 12 20 28 + | | | | + 0 [----------> + """ + design = self.design + design.draw_strand(0, 0).move(8).with_insertions([(2, 3), (4, 1)]) + design.inline_deletions_insertions() + self.assert_helix0_strand0_inlined(design, max_offset=28, major_ticks=[0, 12, 20, 28], start=0, + end=12) + + def test_inline_deletions_insertions__one_deletion_one_insertion(self) -> None: + """ + before + 0 2 4 8 16 24 + | | | | + 0 [-3-X--> + + after + 0 10 18 26 + | | | | + 0 [--------> + """ + design = self.design + design.draw_strand(0, 0).move(8).with_deletions(4).with_insertions((2, 3)) + design.inline_deletions_insertions() + self.assert_helix0_strand0_inlined(design, max_offset=26, major_ticks=[0, 10, 18, 26], start=0, + end=10) + + def test_inline_deletions_insertions__one_deletion_right_of_major_tick(self) -> None: + """ + before + 0 89 16 24 + | | | | + 0 [--------X-> + + after + 0 8 15 23 + | | | | + 0 [---------> + """ + design = self.design + design.draw_strand(0, 0).move(12).with_deletions(9) + design.inline_deletions_insertions() + self.assert_helix0_strand0_inlined(design, max_offset=23, major_ticks=[0, 8, 15, 23], start=0, end=11) + + def test_inline_deletions_insertions__one_deletion_on_major_tick(self) -> None: + """ + | is major tick, and . is minor tick + before + 0 8 16 24 + | . . . . . . . | . . . . . . . | . . . . . . . | + [ - - - - - - - X - - > + + after + 0 8 15 23 + | . . . . . . . | . . . . . . | . . . . . . . | + [ - - - - - - - - - > + """ + design = self.design + design.draw_strand(0, 0).move(12).with_deletions(8) + design.inline_deletions_insertions() + self.assert_helix0_strand0_inlined(design, max_offset=23, major_ticks=[0, 8, 15, 23], start=0, end=11) + + def test_inline_deletions_insertions__one_deletion_left_of_major_tick(self) -> None: + """ + before + 0 78 16 24 + | | | | + 0 [------X---> + + after + 0 8 15 23 + | | | | + 0 [---------> + """ + design = self.design + design.draw_strand(0, 0).move(12).with_deletions(7) + design.inline_deletions_insertions() + self.assert_helix0_strand0_inlined(design, max_offset=23, major_ticks=[0, 7, 15, 23], start=0, end=11) + + def test_inline_deletions_insertions__one_insertion_right_of_major_tick(self) -> None: + """ + before + 0 89 16 24 + | | | | + 0 [--------1-> + + after + 0 8 17 25 + | | | | + 0 [-----------> + """ + design = self.design + design.draw_strand(0, 0).move(12).with_insertions((9, 1)) + design.inline_deletions_insertions() + self.assert_helix0_strand0_inlined(design, max_offset=25, major_ticks=[0, 8, 17, 25], start=0, end=13) + + def test_inline_deletions_insertions__one_insertion_on_major_tick(self) -> None: + """ + before + 0 8 16 24 + | | | | + 0 [-------1--> + + after + 0 8 17 25 + | | | | + 0 [-----------> + """ + design = self.design + design.draw_strand(0, 0).move(12).with_insertions((8, 1)) + design.inline_deletions_insertions() + self.assert_helix0_strand0_inlined(design, max_offset=25, major_ticks=[0, 8, 17, 25], start=0, end=13) + + def test_inline_deletions_insertions__one_insertion_left_of_major_tick(self) -> None: + """ + before + 0 78 16 24 + | | | | + 0 [------1---> + + after + 0 9 17 25 + | | | | + 0 [-----------> + """ + design = self.design + design.draw_strand(0, 0).move(12).with_insertions((7, 1)) + design.inline_deletions_insertions() + self.assert_helix0_strand0_inlined(design, max_offset=25, major_ticks=[0, 9, 17, 25], start=0, end=13) + + def test_inline_deletions_insertions__deletions_insertions_in_multiple_domains(self) -> None: + """ + before + 0 5 8 11 16 19 24 + | | | | + 0 [----2-----1-------X----> + + after + 0 10 19 26 + | | | | + 0 [-------------------------> + """ + design = self.design + design.draw_strand(0, 0).move(24).with_deletions(19).with_insertions([(5, 2), (11, 1)]) + design.inline_deletions_insertions() + self.assert_helix0_strand0_inlined(design, max_offset=26, major_ticks=[0, 10, 19, 26], start=0, + end=26) + + def test_inline_deletions_insertions__deletions_insertions_in_multiple_domains_two_strands(self) -> None: + """ + | is major tick, . is minor tick + before + 0 2 5 8 10 16 19 24 + | . . . . . . . | . . . . . . . | . . . . . . . | + [ - X - - 2 - - - - 1 - - > [ - - - - X - - - > + + after + 0 2 5 9 16 18 25 + | . . . . . . . . | . . . . . . . . | . . . . . . | + [ - - - - - - - - - - - - - - > [ - - - - - - - > + """ + design = self.design + design.draw_strand(0, 0).move(14).with_deletions(2).with_insertions([(5, 2), (10, 1)]) + design.draw_strand(0, 14).to(24).with_deletions(19) + design.inline_deletions_insertions() + self.assertEqual(1, len(design.helices)) + self.assertEqual(2, len(design.strands)) + helix = design.helices[0] + strand0 = design.strands[0] + strand1 = design.strands[1] + self.assertEqual(25, helix.max_offset) + self.assertEqual([0, 9, 18, 25], helix.major_ticks) + self.assertEqual(0, strand0.domains[0].start) + self.assertEqual(16, strand0.domains[0].end) + self.assertEqual(16, strand1.domains[0].start) + self.assertEqual(25, strand1.domains[0].end) + self.assertListEqual([], strand0.domains[0].deletions) + self.assertListEqual([], strand0.domains[0].insertions) + self.assertListEqual([], strand1.domains[0].deletions) + self.assertListEqual([], strand1.domains[0].insertions) + + +class TestNickLigateAndCrossover(unittest.TestCase): + """ + Tests add_nick(), ligate(), add_*_crossover(), and remove_*_crossover() methods on Design + """ + + r""" + small_design: + 0 8 16 + ACGTACGA AACCGGTA +0 [------- -------> + <------- -------] + TGCATGCT TTGGCCAT + + AAACCCGG TTTGGGCC +1 [------- -------> + <------- -------] + TTTGGGCC AAACCCGG + + + small_nicked_design + 0 8 16 + ACGTACGA AACCGGTA +0 [------> [------> + <------] <------] + TGCATGCT TTGGCCAT + + AAACCCGG TTTGGGCC +1 [------> [------> + <------] <------] + TTTGGGCC AAACCCGG + + + design: + 0 8 16 24 32 40 48 56 64 72 80 88 96 +0 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> + <------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------] + +1 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> + <------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------] + +2 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> + <------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------] + +3 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> + <------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------] + +4 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> + <------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------] + +5 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> + <------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------] + + two_strand_design: + 0 8 16 + +0 [------- -------> + + +1 <------- -------] + """ + + def setUp(self) -> None: + # small design + strands_small_design = [ + sc.Strand([sc.Domain(0, True, 0, 16)]), + sc.Strand([sc.Domain(0, False, 0, 16)]), + sc.Strand([sc.Domain(1, True, 0, 16)]), + sc.Strand([sc.Domain(1, False, 0, 16)]), + ] + self.small_design = sc.Design(strands=strands_small_design, grid=sc.square) + self.small_design.assign_dna(strands_small_design[0], "ACGTACGA AACCGGTA") + self.small_design.assign_dna(strands_small_design[2], "AAACCCGG TTTGGGCC") + + # small nicked design + small_nicked_helices = [sc.Helix(max_offset=100) for _ in range(2)] + self.small_nicked_design = sc.Design(helices=small_nicked_helices, grid=sc.square) + # forward strands + self.small_nicked_design.draw_strand(0, 0).move(8) + self.small_nicked_design.draw_strand(0, 8).move(8) + self.small_nicked_design.draw_strand(1, 0).move(8) + self.small_nicked_design.draw_strand(1, 8).move(8) + # reverse strands + self.small_nicked_design.draw_strand(0, 8).move(-8) + self.small_nicked_design.draw_strand(0, 16).move(-8) + self.small_nicked_design.draw_strand(1, 8).move(-8) + self.small_nicked_design.draw_strand(1, 16).move(-8) + + self.small_nicked_design.assign_dna(self.small_nicked_design.strands[0], "ACGTACGA") + self.small_nicked_design.assign_dna(self.small_nicked_design.strands[1], "AACCGGTA") + self.small_nicked_design.assign_dna(self.small_nicked_design.strands[2], "AAACCCGG") + self.small_nicked_design.assign_dna(self.small_nicked_design.strands[3], "TTTGGGCC") + + # origami + self.max_offset: int = 8 * 12 + scafs = [] + staps = [] + for helix in range(6): + scaf_ss = sc.Domain(helix, helix % 2 == 0, 0, self.max_offset) + stap_ss = sc.Domain(helix, helix % 2 == 1, 0, self.max_offset) + scaf = sc.Strand([scaf_ss]) + stap = sc.Strand([stap_ss]) + scafs.append(scaf) + staps.append(stap) + self.origami: sc.Design = sc.Design(strands=scafs + staps, grid=sc.square) + + self.two_strand_design = sc.Design(helices=[sc.Helix(max_offset=100) for _ in range(2)]) + self.two_strand_design.draw_strand(0, 0).move(16) + self.two_strand_design.draw_strand(1, 16).move(-16) + + def test_add_nick__twice_on_same_domain(self) -> None: + """ + before + 0 8 16 24 + 0 [------- -------- -------> + + after + 0 8 16 24 + 0 [------> [------> [------> + """ + design = sc.Design(strands=[ + sc.Strand([sc.Domain(0, True, 0, 24)]), + ], grid=sc.square) + design.add_nick(helix=0, offset=8, forward=True) + design.add_nick(helix=0, offset=16, forward=True) + self.assertEqual(3, len(design.strands)) + self.assertIn(sc.Strand([sc.Domain(0, True, 0, 8)]), design.strands) + self.assertIn(sc.Strand([sc.Domain(0, True, 8, 16)]), design.strands) + self.assertIn(sc.Strand([sc.Domain(0, True, 16, 24)]), design.strands) + + def test_ligate__twice_on_same_domain(self) -> None: + """ + before + 0 8 16 24 + 0 [------> [------> [------> + + after + 0 8 16 24 + 0 [------- -------- -------> + """ + design = sc.Design(helices=[sc.Helix(max_offset=24)], grid=sc.square) + design.draw_strand(0, 0).move(8) + design.draw_strand(0, 8).move(8) + design.draw_strand(0, 16).move(8) + design.ligate(helix=0, offset=8, forward=True) + design.ligate(helix=0, offset=16, forward=True) + self.assertEqual(1, len(design.strands)) + self.assertIn(sc.Strand([sc.Domain(0, True, 0, 24)]), design.strands) + + def test_add_nick__small_design_no_nicks_added_yet(self) -> None: + """ + 0 8 16 + ACGTACGA AACCGGTA + 0 [------- -------> + <------- -------] + TGCATGCT TTGGCCAT + + AAACCCGG TTTGGGCC + 1 [------- -------> + <------- -------] + TTTGGGCC AAACCCGG + """ + self.assertEqual(4, len(self.small_design.strands)) + self.assertIn( + sc.Strand([sc.Domain(0, True, 0, 16, dna_sequence=remove_whitespace('ACGTACGA AACCGGTA'))]), + self.small_design.strands) + self.assertIn( + sc.Strand([sc.Domain(0, False, 0, 16, dna_sequence=remove_whitespace('TACCGGTT TCGTACGT'))]), + self.small_design.strands) + self.assertIn( + sc.Strand([sc.Domain(1, True, 0, 16, dna_sequence=remove_whitespace('AAACCCGG TTTGGGCC'))]), + self.small_design.strands) + self.assertIn( + sc.Strand([sc.Domain(1, False, 0, 16, dna_sequence=remove_whitespace('GGCCCAAA CCGGGTTT'))]), + self.small_design.strands) + # DNA + strand = strand_matching(self.small_design.strands, 0, True, 0, 16) + self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) + strand = strand_matching(self.small_design.strands, 0, False, 0, 16) + self.assertEqual(remove_whitespace('TACCGGTT TCGTACGT'), strand.dna_sequence) + strand = strand_matching(self.small_design.strands, 1, True, 0, 16) + self.assertEqual(remove_whitespace('AAACCCGG TTTGGGCC'), strand.dna_sequence) + strand = strand_matching(self.small_design.strands, 1, False, 0, 16) + self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) + + def test_ligate__small_nicked_design_no_ligation_yet(self) -> None: + """ + 0 8 16 + ACGTACGA AACCGGTA + 0 [------> [------> + <------] <------] + TGCATGCT TTGGCCAT + + AAACCCGG TTTGGGCC + 1 [------> [------> + <------] <------] + TTTGGGCC AAACCCGG + """ + design = self.small_nicked_design + self.assertEqual(8, len(design.strands)) + self.assertIn(sc.Strand([sc.Domain(0, True, 0, 8, dna_sequence='ACGTACGA')]), design.strands) + self.assertIn(sc.Strand([sc.Domain(0, False, 0, 8, dna_sequence='TGCATGCT'[::-1]), ]), design.strands) + self.assertIn(sc.Strand([sc.Domain(1, True, 0, 8, dna_sequence='AAACCCGG')]), design.strands) + self.assertIn(sc.Strand([sc.Domain(1, False, 0, 8, dna_sequence='TTTGGGCC'[::-1])]), design.strands) + self.assertIn(sc.Strand([sc.Domain(0, True, 8, 16, dna_sequence='AACCGGTA')]), design.strands) + self.assertIn(sc.Strand([sc.Domain(0, False, 8, 16, dna_sequence='TTGGCCAT'[::-1])]), design.strands) + self.assertIn(sc.Strand([sc.Domain(1, True, 8, 16, dna_sequence='TTTGGGCC')]), design.strands) + self.assertIn(sc.Strand([sc.Domain(1, False, 8, 16, dna_sequence='AAACCCGG'[::-1])]), design.strands) + # DNA + strand = strand_matching(design.strands, 0, True, 0, 8) + self.assertEqual(remove_whitespace('ACGTACGA'), strand.dna_sequence) + strand = strand_matching(design.strands, 0, True, 8, 16) + self.assertEqual(remove_whitespace('AACCGGTA'), strand.dna_sequence) + + strand = strand_matching(design.strands, 0, False, 0, 8) + self.assertEqual(remove_whitespace('TCGTACGT'), strand.dna_sequence) + strand = strand_matching(design.strands, 0, False, 8, 16) + self.assertEqual(remove_whitespace('TACCGGTT'), strand.dna_sequence) + + strand = strand_matching(design.strands, 1, True, 0, 8) + self.assertEqual(remove_whitespace('AAACCCGG'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, True, 8, 16) + self.assertEqual(remove_whitespace('TTTGGGCC'), strand.dna_sequence) + + strand = strand_matching(design.strands, 1, False, 0, 8) + self.assertEqual(remove_whitespace('CCGGGTTT'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, False, 8, 16) + self.assertEqual(remove_whitespace('GGCCCAAA'), strand.dna_sequence) + + def test_add_nick__small_design_H0_forward(self) -> None: + """ + 0 8 16 + ACGTACGA AACCGGTA + 0 [------> [------> + <------- -------] + TGCATGCT TTGGCCAT + + AAACCCGG TTTGGGCC + 1 [------- -------> + <------- -------] + TTTGGGCC AAACCCGG + """ + + design = self.small_design + design.add_nick(helix=0, offset=8, forward=True) + self.assertEqual(5, len(design.strands)) + # two new Strands + self.assertIn(sc.Strand([sc.Domain(0, True, 0, 8, dna_sequence='ACGTACGA')]), design.strands) + self.assertIn(sc.Strand([sc.Domain(0, True, 8, 16, dna_sequence='AACCGGTA')]), design.strands) + # existing Strands + self.assertIn( + sc.Strand([sc.Domain(0, False, 0, 16, dna_sequence=remove_whitespace('TACCGGTT TCGTACGT'))]), + design.strands) + self.assertIn( + sc.Strand([sc.Domain(1, True, 0, 16, dna_sequence=remove_whitespace('AAACCCGG TTTGGGCC'))]), + design.strands) + self.assertIn( + sc.Strand([sc.Domain(1, False, 0, 16, dna_sequence=remove_whitespace('GGCCCAAA CCGGGTTT'))]), + design.strands) + # DNA + strand = strand_matching(design.strands, 0, True, 0, 8) + self.assertEqual(remove_whitespace('ACGTACGA'), strand.dna_sequence) + strand = strand_matching(design.strands, 0, True, 8, 16) + self.assertEqual(remove_whitespace('AACCGGTA'), strand.dna_sequence) + strand = strand_matching(design.strands, 0, False, 0, 16) + self.assertEqual(remove_whitespace('TACCGGTT TCGTACGT'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, True, 0, 16) + self.assertEqual(remove_whitespace('AAACCCGG TTTGGGCC'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, False, 0, 16) + self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) + + def test_ligate__small_nicked_design_ligate_all(self) -> None: + """ + 0 8 16 + ACGTACGA AACCGGTA + 0 [------- -------> + <------- -------] + TGCATGCT TTGGCCAT + + AAACCCGG TTTGGGCC + 1 [------- -------> + <------- -------] + TTTGGGCC AAACCCGG + """ + design = self.small_nicked_design + design.ligate(0, 8, True) + design.ligate(0, 8, False) + design.ligate(1, 8, True) + design.ligate(1, 8, False) + self.assertEqual(4, len(design.strands)) + self.assertIn(sc.Strand([sc.Domain(0, True, 0, 16, dna_sequence='ACGTACGAAACCGGTA')]), design.strands) + self.assertIn( + sc.Strand([sc.Domain(0, False, 0, 16, dna_sequence='TGCATGCTTTGGCCAT'[:: -1])]), + design.strands) + self.assertIn(sc.Strand([sc.Domain(1, True, 0, 16, dna_sequence='AAACCCGGTTTGGGCC')]), design.strands) + self.assertIn( + sc.Strand([sc.Domain(1, False, 0, 16, dna_sequence='TTTGGGCCAAACCCGG'[:: -1])]), + design.strands) + # DNA + strand = strand_matching(design.strands, 0, True, 0, 16) + self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) + strand = strand_matching(design.strands, 0, False, 0, 16) + self.assertEqual(remove_whitespace('TACCGGTT TCGTACGT'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, True, 0, 16) + self.assertEqual(remove_whitespace('AAACCCGG TTTGGGCC'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, False, 0, 16) + self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) + + def test_add_nick__small_design_H0_reverse(self) -> None: + """ + 0 8 16 + ACGTACGA AACCGGTA + 0 [------- -------> + <------] <------] + TGCATGCT TTGGCCAT + + AAACCCGG TTTGGGCC + 1 [------- -------> + <------- -------] + TTTGGGCC AAACCCGG + """ + design = self.small_design + design.add_nick(helix=0, offset=8, forward=False) + self.assertEqual(5, len(design.strands)) + # two new Strands + self.assertIn( + sc.Strand([sc.Domain(0, False, 0, 8, dna_sequence=remove_whitespace('TCGTACGT'))]), + design.strands) + self.assertIn( + sc.Strand([sc.Domain(0, False, 8, 16, dna_sequence=remove_whitespace('TACCGGTT'))]), + design.strands) + # existing Strands + self.assertIn( + sc.Strand([sc.Domain(0, True, 0, 16, dna_sequence=remove_whitespace('ACGTACGA AACCGGTA'))]), + design.strands) + self.assertIn( + sc.Strand([sc.Domain(1, True, 0, 16, dna_sequence=remove_whitespace('AAACCCGG TTTGGGCC'))]), + design.strands) + self.assertIn( + sc.Strand([sc.Domain(1, False, 0, 16, dna_sequence=remove_whitespace('GGCCCAAA CCGGGTTT'))]), + design.strands) + # DNA + strand = strand_matching(design.strands, 0, True, 0, 16) + self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) + strand = strand_matching(design.strands, 0, False, 8, 16) + self.assertEqual(remove_whitespace('TACCGGTT'), strand.dna_sequence) + strand = strand_matching(design.strands, 0, False, 0, 8) + self.assertEqual(remove_whitespace('TCGTACGT'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, True, 0, 16) + self.assertEqual(remove_whitespace('AAACCCGG TTTGGGCC'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, False, 0, 16) + self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) + + def test_add_nick__small_design_H1_forward(self) -> None: + """ + 0 8 16 + ACGTACGA AACCGGTA + 0 [------- -------> + <------- -------] + TGCATGCT TTGGCCAT + + AAACCCGG TTTGGGCC + 1 [------> [------> + <------- -------] + TTTGGGCC AAACCCGG + """ + design = self.small_design + design.add_nick(helix=1, offset=8, forward=True) + self.assertEqual(5, len(design.strands)) + # two new Strands + self.assertIn( + sc.Strand([sc.Domain(1, True, 0, 8, dna_sequence=remove_whitespace('AAACCCGG'))]), + design.strands) + self.assertIn( + sc.Strand([sc.Domain(1, True, 8, 16, dna_sequence=remove_whitespace('TTTGGGCC'))]), + design.strands) + # existing Strands + self.assertIn( + sc.Strand([sc.Domain(1, False, 0, 16, dna_sequence=remove_whitespace('GGCCCAAA CCGGGTTT'))]), + design.strands) + self.assertIn( + sc.Strand([sc.Domain(0, True, 0, 16, dna_sequence=remove_whitespace('ACGTACGA AACCGGTA'))]), + design.strands) + self.assertIn( + sc.Strand([sc.Domain(0, False, 0, 16, dna_sequence=remove_whitespace('TACCGGTT TCGTACGT'))]), + design.strands) + # DNA + strand = strand_matching(design.strands, 0, True, 0, 16) + self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) + strand = strand_matching(design.strands, 0, False, 0, 16) + self.assertEqual(remove_whitespace('TACCGGTT TCGTACGT'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, True, 0, 8) + self.assertEqual(remove_whitespace('AAACCCGG'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, True, 8, 16) + self.assertEqual(remove_whitespace('TTTGGGCC'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, False, 0, 16) + self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) + + def test_add_nick__small_design_H1_reverse(self) -> None: + """ + 0 8 16 + ACGTACGA AACCGGTA + 0 [------- -------> + <------- -------] + TGCATGCT TTGGCCAT + + AAACCCGG TTTGGGCC + 1 [------- -------> + <------] <------] + TTTGGGCC AAACCCGG + """ + design = self.small_design + design.add_nick(helix=1, offset=8, forward=False) + self.assertEqual(5, len(design.strands)) + # two new Strands + self.assertIn( + sc.Strand([sc.Domain(1, False, 0, 8, dna_sequence=remove_whitespace('CCGGGTTT'))]), + design.strands) + self.assertIn( + sc.Strand([sc.Domain(1, False, 8, 16, dna_sequence=remove_whitespace('GGCCCAAA'))]), + design.strands) + # existing Strands + self.assertIn( + sc.Strand([sc.Domain(1, True, 0, 16, dna_sequence=remove_whitespace('AAACCCGG TTTGGGCC'))]), + design.strands) + self.assertIn( + sc.Strand([sc.Domain(0, True, 0, 16, dna_sequence=remove_whitespace('ACGTACGA AACCGGTA'))]), + design.strands) + self.assertIn( + sc.Strand([sc.Domain(0, False, 0, 16, dna_sequence=remove_whitespace('TACCGGTT TCGTACGT'))]), + design.strands) + # DNA + strand = strand_matching(design.strands, 0, True, 0, 16) + self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) + strand = strand_matching(design.strands, 0, False, 0, 16) + self.assertEqual(remove_whitespace('TACCGGTT TCGTACGT'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, True, 0, 16) + self.assertEqual(remove_whitespace('AAACCCGG TTTGGGCC'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, False, 0, 8) + self.assertEqual(remove_whitespace('CCGGGTTT'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, False, 8, 16) + self.assertEqual(remove_whitespace('GGCCCAAA'), strand.dna_sequence) + + def test_add_full_crossover__small_design_H0_forward(self) -> None: + """ + 0 8 16 + ACGTACGA AACCGGTA + 0 [------+ +------> + <------- -------] + TGCATGCT TTGGCCAT + | | + AAACCCGG TTTGGGCC + 1 [------- -------> + <------+ +------] + TTTGGGCC AAACCCGG + """ + design = self.small_design + design.add_full_crossover(helix=0, helix2=1, offset=8, forward=True) + self.assertEqual(4, len(design.strands)) + # two new Strands + self.assertIn(sc.Strand([ + sc.Domain(0, True, 0, 8, dna_sequence=remove_whitespace('ACGTACGA')), + sc.Domain(1, False, 0, 8, dna_sequence=remove_whitespace('TTTGGGCC'[::-1])), + ]), design.strands) + self.assertIn(sc.Strand([ + sc.Domain(1, False, 8, 16, dna_sequence=remove_whitespace('AAACCCGG'[::-1])), + sc.Domain(0, True, 8, 16, dna_sequence=remove_whitespace('AACCGGTA')), + ]), design.strands) + # existing Strands + self.assertIn(sc.Strand([ + sc.Domain(0, False, 0, 16, dna_sequence=remove_whitespace('TGCATGCT TTGGCCAT'[::-1])) + ]), design.strands) + self.assertIn(sc.Strand([ + sc.Domain(1, True, 0, 16, dna_sequence=remove_whitespace('AAACCCGG TTTGGGCC')) + ]), design.strands) + # DNA + strand = strand_matching(design.strands, 0, False, 0, 16) + self.assertEqual(remove_whitespace('TACCGGTT TCGTACGT'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, True, 0, 16) + self.assertEqual(remove_whitespace('AAACCCGG TTTGGGCC'), strand.dna_sequence) + strand = strand_matching(design.strands, 0, True, 0, 8) + self.assertEqual(remove_whitespace('ACGTACGA CCGGGTTT'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, False, 8, 16) + self.assertEqual(remove_whitespace('GGCCCAAA AACCGGTA'), strand.dna_sequence) + + def test_add_full_crossover__small_design_H0_reverse(self) -> None: + """ + 0 8 16 + ACGTACGA AACCGGTA + 0 [------- -------> + <------+ +------] + TGCATGCT TTGGCCAT + | | + AAACCCGG TTTGGGCC + 1 [------+ +------> + <------- -------] + TTTGGGCC AAACCCGG + """ + design = self.small_design + design.add_full_crossover(helix=0, helix2=1, offset=8, forward=False) + self.assertEqual(4, len(design.strands)) + # two new Strands + self.assertIn(sc.Strand([ + sc.Domain(1, True, 0, 8, dna_sequence='AAACCCGG'), + sc.Domain(0, False, 0, 8, dna_sequence='TGCATGCT'[::-1]), + ]), design.strands) + self.assertIn(sc.Strand([ + sc.Domain(0, False, 8, 16, dna_sequence='TTGGCCAT'[::-1]), + sc.Domain(1, True, 8, 16, dna_sequence='TTTGGGCC'), + ]), design.strands) + # existing Strands + self.assertIn(sc.Strand([ + sc.Domain(0, True, 0, 16, dna_sequence='ACGTACGAAACCGGTA') + ]), design.strands) + self.assertIn(sc.Strand([ + sc.Domain(1, False, 0, 16, dna_sequence='TTTGGGCCAAACCCGG'[:: -1]) + ]), design.strands) + # DNA + strand = strand_matching(design.strands, 0, True, 0, 16) + self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) + strand = strand_matching(design.strands, 0, False, 8, 16) + self.assertEqual(remove_whitespace('TACCGGTT TTTGGGCC'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, True, 0, 8) + self.assertEqual(remove_whitespace('AAACCCGG TCGTACGT'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, False, 0, 16) + self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) + + def test_add_full_crossover__horizontal_crossovers_already_there(self) -> None: + """ + 0 8 16 + 0 [------+^+------> + 1 <------+^+------] + """ + design = sc.Design(helices=[sc.Helix(16) for _ in range(2)]) + design.draw_strand(0, 0).move(8).move(8) + design.draw_strand(1, 16).move(-8).move(-8) + + self.assertEqual(2, len(design.strands)) + + with self.assertRaises(sc.IllegalDesignError) as ctx: + design.add_full_crossover(helix=0, helix2=1, offset=8, forward=True) + msg = str(ctx.exception) + self.assertIn('already a crossover', msg) + + def test_add_full_crossover__top_horizontal_crossover_already_there(self) -> None: + """ + 0 8 16 + 0 [------+^+------> + 1 <------] <------] + """ + design = sc.Design(helices=[sc.Helix(16) for _ in range(2)]) + design.draw_strand(0, 0).move(8).move(8) + design.draw_strand(1, 16).move(-8) + design.draw_strand(1, 8).move(-8) + + self.assertEqual(3, len(design.strands)) + + with self.assertRaises(sc.IllegalDesignError) as ctx: + design.add_full_crossover(helix=0, helix2=1, offset=8, forward=True) + msg = str(ctx.exception) + self.assertIn('already a crossover', msg) + + def test_add_full_crossover__bottom_horizontal_crossover_already_there(self) -> None: + """ + 0 8 16 + 0 [------> [------> + 1 <------+^+------] + """ + design = sc.Design(helices=[sc.Helix(16) for _ in range(2)]) + design.draw_strand(0, 0).move(8) + design.draw_strand(0, 8).move(8) + design.draw_strand(1, 16).move(-8).move(-8) + + self.assertEqual(3, len(design.strands)) + + with self.assertRaises(sc.IllegalDesignError) as ctx: + design.add_full_crossover(helix=0, helix2=1, offset=8, forward=True) + msg = str(ctx.exception) + self.assertIn('already a crossover', msg) + + def test_add_half_crossover__horizontal_crossovers_already_there(self) -> None: + """ + 0 8 16 + 0 [------+^+------> + 1 <------+^+------] + """ + design = sc.Design(helices=[sc.Helix(16) for _ in range(2)]) + design.draw_strand(0, 0).move(8).move(8) + design.draw_strand(1, 16).move(-8).move(-8) + + self.assertEqual(2, len(design.strands)) + + with self.assertRaises(sc.IllegalDesignError) as ctx: + design.add_half_crossover(helix=0, helix2=1, offset=8, forward=True) + msg = str(ctx.exception) + self.assertIn('is expected to be on the', msg) # both 3' and 5' are problems, so just make sure + self.assertIn('end of the strand', msg) # one of them is mentioned here + + def test_add_half_crossover__top_horizontal_crossover_already_there(self) -> None: + """ + 0 8 16 + 0 [------+^+------> + 1 <------] <------] + """ + design = sc.Design(helices=[sc.Helix(16) for _ in range(2)]) + design.draw_strand(0, 0).move(8).move(8) + design.draw_strand(1, 16).move(-8) + design.draw_strand(1, 8).move(-8) + + self.assertEqual(3, len(design.strands)) + + with self.assertRaises(sc.IllegalDesignError) as ctx: + design.add_half_crossover(helix=0, helix2=1, offset=8, forward=True) + msg = str(ctx.exception) + self.assertIn("is expected to be on the 5' end of the strand", msg) + + def test_add_half_crossover__bottom_horizontal_crossover_already_there(self) -> None: + """ + 0 8 16 + 0 [------> [------> + 1 <------+^+------] + """ + design = sc.Design(helices=[sc.Helix(16) for _ in range(2)]) + design.draw_strand(0, 0).move(8) + design.draw_strand(0, 8).move(8) + design.draw_strand(1, 16).move(-8).move(-8) + + self.assertEqual(3, len(design.strands)) + + with self.assertRaises(sc.IllegalDesignError) as ctx: + design.add_half_crossover(helix=0, helix2=1, offset=8, forward=True) + msg = str(ctx.exception) + self.assertIn("is expected to be on the 3' end of the strand", msg) + + def test_add_half_crossover__small_design_H0_reverse_8(self) -> None: + """ + 0 8 16 + ACGTACGA AACCGGTA + 0 [------- -------> + <------] +------] + TGCATGCT TTGGCCAT + | + AAACCCGG TTTGGGCC + 1 [------> +------> + <------- -------] + TTTGGGCC AAACCCGG + """ + design = self.small_design + design.add_nick(helix=0, offset=8, forward=False) + design.add_nick(helix=1, offset=8, forward=True) + design.add_half_crossover(helix=0, helix2=1, offset=8, forward=False) + self.assertEqual(5, len(design.strands)) + # three new Strands + self.assertIn(sc.Strand([ + sc.Domain(1, True, 0, 8, dna_sequence=remove_whitespace('AAACCCGG')), + ]), design.strands) + self.assertIn(sc.Strand([ + sc.Domain(0, False, 0, 8, dna_sequence=remove_whitespace('TCGTACGT')), + ]), design.strands) + self.assertIn(sc.Strand([ + sc.Domain(0, False, 8, 16, dna_sequence=remove_whitespace('TACCGGTT')), + sc.Domain(1, True, 8, 16, dna_sequence=remove_whitespace('TTTGGGCC')), + ]), design.strands) + # existing Strands + self.assertIn(sc.Strand([ + sc.Domain(0, True, 0, 16, dna_sequence=remove_whitespace('ACGTACGA AACCGGTA')), + ]), design.strands) + self.assertIn(sc.Strand([ + sc.Domain(1, False, 0, 16, dna_sequence=remove_whitespace('GGCCCAAA CCGGGTTT')), + ]), design.strands) + # DNA + strand = strand_matching(design.strands, 0, True, 0, 16) + self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) + strand = strand_matching(design.strands, 0, False, 8, 16) + self.assertEqual(remove_whitespace('TACCGGTT TTTGGGCC'), strand.dna_sequence) + strand = strand_matching(design.strands, 0, False, 0, 8) + self.assertEqual(remove_whitespace('TCGTACGT'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, True, 0, 8) + self.assertEqual(remove_whitespace('AAACCCGG'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, False, 0, 16) + self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) + + def test_add_half_crossover__both_scaffolds_preserved(self) -> None: + """ + 0 8 16 + + 0 [------- -------> scaffold + + + 1 <------- -------] scaffold + + """ + design: sc.Design = self.two_strand_design + design.strands[0].set_scaffold(True) + design.strands[1].set_scaffold(True) + design.add_half_crossover(helix=0, helix2=1, offset=15, forward=True) + self.assertEqual(1, len(design.strands)) + self.assertTrue(design.strands[0].is_scaffold) + self.assertIsNotNone(design.scaffold) + + def test_add_half_crossover__first_scaffold_preserved(self) -> None: + """ + 0 8 16 + + 0 [------- -------> scaffold + + + 1 <------- -------] + + """ + design: sc.Design = self.two_strand_design + design.strands[0].set_scaffold(True) + design.add_half_crossover(helix=0, helix2=1, offset=15, forward=True) + self.assertEqual(1, len(design.strands)) + self.assertTrue(design.strands[0].is_scaffold) + self.assertIsNotNone(design.scaffold) + + def test_add_half_crossover__second_scaffold_preserved(self) -> None: + """ + 0 8 16 + + 0 [------- -------> + + + 1 <------- -------] scaffold + + """ + design: sc.Design = self.two_strand_design + design.strands[1].set_scaffold(True) + design.add_half_crossover(helix=0, helix2=1, offset=15, forward=True) + self.assertEqual(1, len(design.strands)) + self.assertTrue(design.strands[0].is_scaffold) + self.assertIsNotNone(design.scaffold) + + def test_add_half_crossover__neither_scaffold_preserved(self) -> None: + """ + 0 8 16 + + 0 [------- -------> + + + 1 <------- -------] + + """ + design: sc.Design = self.two_strand_design + design.add_half_crossover(helix=0, helix2=1, offset=15, forward=True) + self.assertEqual(1, len(design.strands)) + self.assertFalse(design.strands[0].is_scaffold) + self.assertIsNone(design.scaffold) + + def test_add_half_crossover__small_design_H0_reverse_0(self) -> None: + """ + 0 8 16 + ACGTACGA AACCGGTA + 0 [------- -------> + +------- -------] + TGCATGCT TTGGCCAT + | + AAACCCGG TTTGGGCC + 1 -------- -------> + <------- -------] + TTTGGGCC AAACCCGG + """ + design = self.small_design + design.add_half_crossover(helix=0, helix2=1, offset=0, forward=False) + self.assertEqual(3, len(design.strands)) + # one new Strand + self.assertIn(sc.Strand([ + sc.Domain(0, False, 0, 16, dna_sequence=remove_whitespace('TGCATGCT TTGGCCAT'[::-1])), + sc.Domain(1, True, 0, 16, dna_sequence=remove_whitespace('AAACCCGG TTTGGGCC')), + ]), design.strands) + # existing Strands + self.assertIn(sc.Strand([ + sc.Domain(0, True, 0, 16, dna_sequence=remove_whitespace('ACGTACGA AACCGGTA')), + ]), design.strands) + self.assertIn(sc.Strand([ + sc.Domain(1, False, 0, 16, dna_sequence=remove_whitespace('TTTGGGCC AAACCCGG'[::-1])), + ]), design.strands) + # DNA + strand = strand_matching(design.strands, 0, True, 0, 16) + self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) + strand = strand_matching(design.strands, 0, False, 0, 16) + self.assertEqual(remove_whitespace('TACCGGTT TCGTACGT AAACCCGG TTTGGGCC'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, False, 0, 16) + self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) + + def test_add_half_crossover__small_design_H0_reverse_15(self) -> None: + """ + 0 8 16 + ACGTACGA AACCGGTA + 0 [------- -------> + <------- -------+ + TGCATGCT TTGGCCAT + | + AAACCCGG TTTGGGCC + 1 [------- -------+ + <------- -------] + TTTGGGCC AAACCCGG + """ + design = self.small_design + design.add_half_crossover(helix=0, helix2=1, offset=15, forward=False) + self.assertEqual(3, len(design.strands)) + # one new Strand + self.assertIn(sc.Strand([ + sc.Domain(1, True, 0, 16, dna_sequence=remove_whitespace('AAACCCGG TTTGGGCC')), + sc.Domain(0, False, 0, 16, dna_sequence=remove_whitespace('TGCATGCT TTGGCCAT'[::-1])), + ]), design.strands) + # existing Strands + self.assertIn( + sc.Strand([sc.Domain(0, True, 0, 16, dna_sequence=remove_whitespace('ACGTACGA AACCGGTA'))]), + design.strands) + self.assertIn( + sc.Strand([sc.Domain(1, False, 0, 16, dna_sequence=remove_whitespace('GGCCCAAA CCGGGTTT'))]), + design.strands) + # DNA + strand = strand_matching(design.strands, 0, True, 0, 16) + self.assertEqual(remove_whitespace('ACGTACGA AACCGGTA'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, True, 0, 16) + self.assertEqual(remove_whitespace('AAACCCGG TTTGGGCC TACCGGTT TCGTACGT'), strand.dna_sequence) + strand = strand_matching(design.strands, 1, False, 0, 16) + self.assertEqual(remove_whitespace('GGCCCAAA CCGGGTTT'), strand.dna_sequence) + + def test_add_half_crossover__small_design_illegal(self) -> None: + """ + 0 8 16 + 0 [------- -------> + <------- -------- ? + | + 1 [------- -------- ? + <------- -------] + """ + with self.assertRaises(sc.IllegalDesignError): + self.small_design.add_half_crossover(helix=0, helix2=1, offset=16, forward=False) + + def test_add_full_crossover__small_design_illegal(self) -> None: + """ + 0 8 16 + 0 [------- -------> + <------- -------+ ? + | | + 1 [------- -------+ ? + <------- -------] + """ + with self.assertRaises(sc.IllegalDesignError): + self.small_design.add_full_crossover(helix=0, helix2=1, offset=16, forward=False) + + def test_add_full_crossover__small_design_illegal_only_one_helix_has_domain(self) -> None: + """ + 0 8 16 + 0 [------- -------> + <------+ +------] + | | + 1 [---> ? ? + <---] + """ + design = sc.Design(strands=[ + sc.Strand([sc.Domain(0, True, 0, 16)]), + sc.Strand([sc.Domain(0, False, 0, 16)]), + sc.Strand([sc.Domain(1, True, 0, 5)]), + sc.Strand([sc.Domain(1, False, 0, 5)]), + ], grid=sc.square) + with self.assertRaises(sc.IllegalDesignError): + design.add_full_crossover(helix=0, helix2=1, offset=10, forward=False) + + r""" + 0 8 16 24 32 40 48 56 64 72 80 88 96 +0 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> + <------- -------- -------- -------- -------] <------- -------- -------- -------] <------- -------- -------] + +1 [------- -------- -------> [------- -------- -------- -------> [------- -------- -------- -------- -------> + <------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------] + +2 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> + <------- -------- -------- -------- -------] <------- -------- -------- -------] <------- -------- -------] + +3 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> + <------- -------- -------> [------- -------- -------- -------> [------- -------- -------- -------- -------] + +4 [------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------> + <------- -------- -------- -------- -------] <------- -------- -------- -------] <------- -------- -------] + +5 [------- -------- -------> [------- -------- -------- -------> [------- -------- -------- -------- -------> + <------- -------- -------- -------- -------- -------] <------- -------- -------- -------- -------- -------] + """ + + @staticmethod + def add_nicks(design: sc.Design): + design.add_nick(helix=5, offset=48, forward=False) + design.add_nick(helix=0, offset=40, forward=False) + design.add_nick(helix=0, offset=72, forward=False) + design.add_nick(helix=2, offset=40, forward=False) + design.add_nick(helix=2, offset=72, forward=False) + design.add_nick(helix=4, offset=40, forward=False) + design.add_nick(helix=4, offset=72, forward=False) + design.add_nick(helix=1, offset=24, forward=True) + design.add_nick(helix=1, offset=56, forward=True) + design.add_nick(helix=3, offset=24, forward=True) + design.add_nick(helix=3, offset=56, forward=True) + design.add_nick(helix=5, offset=24, forward=True) + design.add_nick(helix=5, offset=56, forward=True) + + def test_add_nick__6_helix_rectangle(self) -> None: + self.add_nicks(self.origami) + self.assertEqual(25, len(self.origami.strands)) + for helix in range(0, len(self.origami.helices), 2): + # even helix + self.assertIn(sc.Strand([sc.Domain(helix, True, 0, 96)]), self.origami.strands) + self.assertIn(sc.Strand([sc.Domain(helix, False, 0, 40)]), self.origami.strands) + self.assertIn(sc.Strand([sc.Domain(helix, False, 40, 72)]), self.origami.strands) + self.assertIn(sc.Strand([sc.Domain(helix, False, 72, 96)]), self.origami.strands) + # odd helix + if helix + 1 < len(self.origami.helices) - 1: + self.assertIn(sc.Strand([sc.Domain(helix + 1, False, 0, 96)]), self.origami.strands) + else: + # nick in scaffold on bottom helix + self.assertIn(sc.Strand([sc.Domain(helix + 1, False, 0, 48)]), self.origami.strands) + self.assertIn(sc.Strand([sc.Domain(helix + 1, False, 48, 96)]), self.origami.strands) + self.assertIn(sc.Strand([sc.Domain(helix + 1, True, 0, 24)]), self.origami.strands) + self.assertIn(sc.Strand([sc.Domain(helix + 1, True, 24, 56)]), self.origami.strands) + self.assertIn(sc.Strand([sc.Domain(helix + 1, True, 56, 96)]), self.origami.strands) + + # TODO: re-write this test after support for circular Strands is added and test making crossovers first + r""" + Crossovers needed to be added after nicks so that there will be no circular strands + 0 8 16 24 32 40 48 56 64 72 80 88 96 +0 +------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------- -------+ + /<------- -------+ +------- -------- -------] <------- -------- -------- -------] <------+ +------- -------]\ + ( | | | | ) +1 \[------- -------+ +------> [------+ +------- -------- -------> [------+ +------- -------+ +------- ------->/ + +------- -------- -------- -------- -------- -------+ +------- -------- -------- -------- -------- -------+ + | | | | | | +2 +------- -------- -------- -------- -------- -------+ +------- -------- -------- -------- -------- -------+ + /<------- -------+ +------- -------+ +------] <------- -------- -------+ +------] <------+ +------- -------]\ + ( | | | | ) +3 \[------- -------+ +------> [------+ +------- -------- -------> [------+ +------- -------+ +------- ------->/ + +------- -------- -------- -------- -------- -------+ +------- -------- -------- -------- -------- -------+ + | | | | | | +4 +------- -------- -------- -------- -------- -------+ +------- -------- -------- -------- -------- -------+ + /<------- -------+ +------- -------+ +------] <------- -------- -------+ +------] <------+ +------- -------]\ + ( | | | | ) +5 \[------- -------+ +------> [------- -------- -------- -------> [------- -------- -------+ +------- ------->/ + +------- -------- -------- -------- -------- -------] <------- -------- -------- -------- -------- -------+ + + """ + + @staticmethod + def add_crossovers_after_nicks(design: sc.Design): + # scaffold seam crossovers + design.add_full_crossover(helix=1, helix2=2, offset=48, forward=False) + design.add_full_crossover(helix=3, helix2=4, offset=48, forward=False) + + # staple crossovers + design.add_full_crossover(helix=0, helix2=1, offset=16, forward=False) + design.add_full_crossover(helix=0, helix2=1, offset=80, forward=False) + design.add_full_crossover(helix=1, helix2=2, offset=32, forward=True) + design.add_full_crossover(helix=1, helix2=2, offset=64, forward=True) + design.add_full_crossover(helix=2, helix2=3, offset=16, forward=False) + design.add_full_crossover(helix=2, helix2=3, offset=80, forward=False) + design.add_full_crossover(helix=3, helix2=4, offset=32, forward=True) + design.add_full_crossover(helix=3, helix2=4, offset=64, forward=True) + design.add_full_crossover(helix=4, helix2=5, offset=16, forward=False) + design.add_full_crossover(helix=4, helix2=5, offset=80, forward=False) + + # The left and right edge crossovers need to be added last to ensure the Strands remain + # non-circular during all intermediate stages. + + # scaffold left crossovers + design.add_half_crossover(helix=0, helix2=1, offset=0, forward=True) + design.add_half_crossover(helix=2, helix2=3, offset=0, forward=True) + design.add_half_crossover(helix=4, helix2=5, offset=0, forward=True) + + # scaffold right crossovers + design.add_half_crossover(helix=0, helix2=1, offset=95, forward=True) + design.add_half_crossover(helix=2, helix2=3, offset=95, forward=True) + design.add_half_crossover(helix=4, helix2=5, offset=95, forward=True) + + def test_add_nick_then_add_crossovers__6_helix_rectangle(self) -> None: + self.add_nicks(self.origami) + self.add_crossovers_after_nicks(self.origami) + + self.assertEqual(19, len(self.origami.strands)) + + # staples left edge + # {"helix": 1, "forward": true, "start": 0, "end": 16}, + # {"helix": 0, "forward": false, "start": 0, "end": 16} + stap = sc.Strand([ + sc.Domain(1, True, 0, 16), + sc.Domain(0, False, 0, 16), + ]) + self.assertIn(stap, self.origami.strands) + + # {"helix": 3, "forward": true, "start": 0, "end": 16}, + # {"helix": 2, "forward": false, "start": 0, "end": 16} + stap = sc.Strand([ + sc.Domain(3, True, 0, 16), + sc.Domain(2, False, 0, 16), + ]) + self.assertIn(stap, self.origami.strands) + + # {"helix": 5, "forward": true, "start": 0, "end": 16}, + # {"helix": 4, "forward": false, "start": 0, "end": 16} + stap = sc.Strand([ + sc.Domain(5, True, 0, 16), + sc.Domain(4, False, 0, 16), + ]) + self.assertIn(stap, self.origami.strands) + + # staples right edge + # {"helix": 0, "forward": false, "start": 80, "end": 96}, + # {"helix": 1, "forward": true, "start": 80, "end": 96} + stap = sc.Strand([ + sc.Domain(0, False, 80, 96), + sc.Domain(1, True, 80, 96), + ]) + self.assertIn(stap, self.origami.strands) + + # {"helix": 2, "forward": false, "start": 80, "end": 96}, + # {"helix": 3, "forward": true, "start": 80, "end": 96} + stap = sc.Strand([ + sc.Domain(2, False, 80, 96), + sc.Domain(3, True, 80, 96), + ]) + self.assertIn(stap, self.origami.strands) + + # {"helix": 4, "forward": false, "start": 80, "end": 96}, + # {"helix": 5, "forward": true, "start": 80, "end": 96} + stap = sc.Strand([ + sc.Domain(4, False, 80, 96), + sc.Domain(5, True, 80, 96), + ]) + self.assertIn(stap, self.origami.strands) + + # staples remainder + # {"helix": 0, "forward": false, "start": 40, "end": 72} + stap = sc.Strand([sc.Domain(0, False, 40, 72)]) + self.assertIn(stap, self.origami.strands) + + # {"helix": 2, "forward": false, "start": 32, "end": 40}, + # {"helix": 1, "forward": true, "start": 32, "end": 56} + stap = sc.Strand([ + sc.Domain(2, False, 32, 40), + sc.Domain(1, True, 32, 56), + ]) + self.assertIn(stap, self.origami.strands) + + # {"helix": 1, "forward": true, "start": 56, "end": 64}, + # {"helix": 2, "forward": false, "start": 40, "end": 64} + stap = sc.Strand([ + sc.Domain(1, True, 56, 64), + sc.Domain(2, False, 40, 64), + ]) + self.assertIn(stap, self.origami.strands) + + # {"helix": 4, "forward": false, "start": 32, "end": 40}, + # {"helix": 3, "forward": true, "start": 32, "end": 56} + stap = sc.Strand([ + sc.Domain(4, False, 32, 40), + sc.Domain(3, True, 32, 56), + ]) + self.assertIn(stap, self.origami.strands) + + # {"helix": 3, "forward": true, "start": 56, "end": 64}, + # {"helix": 4, "forward": false, "start": 40, "end": 64} + stap = sc.Strand([ + sc.Domain(3, True, 56, 64), + sc.Domain(4, False, 40, 64), + ]) + self.assertIn(stap, self.origami.strands) + + # {"helix": 5, "forward": true, "start": 24, "end": 56} + stap = sc.Strand([sc.Domain(5, True, 24, 56)]) + self.assertIn(stap, self.origami.strands) + + # {"helix": 0, "forward": false, "start": 16, "end": 40}, + # {"helix": 1, "forward": true, "start": 16, "end": 24} + stap = sc.Strand([ + sc.Domain(0, False, 16, 40), + sc.Domain(1, True, 16, 24), + ]) + self.assertIn(stap, self.origami.strands) + + # {"helix": 1, "forward": true, "start": 24, "end": 32}, + # {"helix": 2, "forward": false, "start": 16, "end": 32}, + # {"helix": 3, "forward": true, "start": 16, "end": 24} + stap = sc.Strand([ + sc.Domain(1, True, 24, 32), + sc.Domain(2, False, 16, 32), + sc.Domain(3, True, 16, 24), + ]) + self.assertIn(stap, self.origami.strands) + + # {"helix": 3, "forward": true, "start": 24, "end": 32}, + # {"helix": 4, "forward": false, "start": 16, "end": 32}, + # {"helix": 5, "forward": true, "start": 16, "end": 24} + stap = sc.Strand([ + sc.Domain(3, True, 24, 32), + sc.Domain(4, False, 16, 32), + sc.Domain(5, True, 16, 24), + ]) + self.assertIn(stap, self.origami.strands) + + # {"helix": 5, "forward": true, "start": 56, "end": 80}, + # {"helix": 4, "forward": false, "start": 72, "end": 80} + stap = sc.Strand([ + sc.Domain(5, True, 56, 80), + sc.Domain(4, False, 72, 80), + ]) + self.assertIn(stap, self.origami.strands) + + # {"helix": 2, "forward": false, "start": 64, "end": 72}, + # {"helix": 1, "forward": true, "start": 64, "end": 80}, + # {"helix": 0, "forward": false, "start": 72, "end": 80} + stap = sc.Strand([ + sc.Domain(2, False, 64, 72), + sc.Domain(1, True, 64, 80), + sc.Domain(0, False, 72, 80), + ]) + self.assertIn(stap, self.origami.strands) + + # {"helix": 4, "forward": false, "start": 64, "end": 72}, + # {"helix": 3, "forward": true, "start": 64, "end": 80}, + # {"helix": 2, "forward": false, "start": 72, "end": 80} + stap = sc.Strand([ + sc.Domain(4, False, 64, 72), + sc.Domain(3, True, 64, 80), + sc.Domain(2, False, 72, 80), + ]) + self.assertIn(stap, self.origami.strands) + + # scaffold + # {"helix": 5, "forward": false, "start": 0, "end": 48}, + # {"helix": 4, "forward": true, "start": 0, "end": 48}, + # {"helix": 3, "forward": false, "start": 0, "end": 48}, + # {"helix": 2, "forward": true, "start": 0, "end": 48}, + # {"helix": 1, "forward": false, "start": 0, "end": 48}, + # {"helix": 0, "forward": true, "start": 0, "end": 96}, + # {"helix": 1, "forward": false, "start": 48, "end": 96}, + # {"helix": 2, "forward": true, "start": 48, "end": 96}, + # {"helix": 3, "forward": false, "start": 48, "end": 96}, + # {"helix": 4, "forward": true, "start": 48, "end": 96}, + # {"helix": 5, "forward": false, "start": 48, "end": 96} + scaf = sc.Strand([ + sc.Domain(5, False, 0, 48), + sc.Domain(4, True, 0, 48), + sc.Domain(3, False, 0, 48), + sc.Domain(2, True, 0, 48), + sc.Domain(1, False, 0, 48), + sc.Domain(0, True, 0, 96), + sc.Domain(1, False, 48, 96), + sc.Domain(2, True, 48, 96), + sc.Domain(3, False, 48, 96), + sc.Domain(4, True, 48, 96), + sc.Domain(5, False, 48, 96), + ]) + self.assertIn(scaf, self.origami.strands) + + def test_ligate_on_middle_domain_should_error_3p_case(self) -> None: + """ + Error to ligate here + | + v + [-----+[-----> + | + <-----+ + """ + 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) + + with self.assertRaises(sc.IllegalDesignError): + design.ligate(0, 10, True) + + def test_ligate_on_middle_domain_should_error_5p_case(self) -> None: + """ + Error to ligate here + | + v + [----->+-----> + | + +-----] + """ + design: sc.Design = sc.Design(helices=[sc.Helix(max_offset=100), sc.Helix(max_offset=100)]) + design.draw_strand(0, 0).to(10) + design.draw_strand(1, 20).to(10).cross(0).to(20) + + with self.assertRaises(sc.IllegalDesignError): + design.ligate(0, 10, True) + + def test_ligate_on_extension_side_should_error(self) -> None: + """ + ↗ + / + / + [-------[-----> + ^ + | + error to ligate here + """ + design: sc.Design = sc.Design(helices=[sc.Helix(max_offset=100)]) + design.draw_strand(0, 0).to(10).extension_3p(5) + design.draw_strand(0, 10).to(20) + with self.assertRaises(sc.IllegalDesignError): + design.ligate(0, 10, True) + + def test_ligate_on_non_extension_side_ok(self) -> None: + """ + Before: + □ + \ + \ + --------->[--------> + + After: + □ + \ + \ + -------------------> + """ + # Setup + design: sc.Design = sc.Design(helices=[sc.Helix(max_offset=100)]) + design.draw_strand(0, 0).extension_5p(5).to(10) + design.draw_strand(0, 10).to(20) + + # Action + design.ligate(0, 10, True) + + # Verify + self.assertEqual(1, len(design.strands)) + actual_substrands = design.strands[0].domains + self.assertEqual(2, len(actual_substrands)) + self.assertEqual(sc.Extension(5), actual_substrands[0]) + self.assertEqual(sc.Domain(0, True, 0, 20), actual_substrands[1]) + + def test_add_full_crossover_extension_ok(self) -> None: + """ + Before: + ↗ + / + / + / + 0 [------- -------- + + 1 <------- -------] + + After: + + ↗ + / + / + / + 0 [------+ +------- + | | + 1 <------+ +------] + """ + # Setup + design: sc.Design = sc.Design( + helices=[sc.Helix(max_offset=100), sc.Helix(max_offset=100)] + ) + design.draw_strand(0, 0).to(16).extension_3p(5) + design.draw_strand(1, 16).to(0) + + # Action + design.add_full_crossover(0, 1, 8, True) + + # Validation + expected_strand_0: sc.Strand = sc.Strand([ + sc.Domain(0, True, 0, 8), + sc.Domain(1, False, 0, 8) + ]) + expected_strand_1: sc.Strand = sc.Strand([ + sc.Domain(1, False, 8, 16), + sc.Domain(0, True, 8, 16), + sc.Extension(5) + ]) + self.assertEqual(2, len(design.strands)) + self.assertIn(expected_strand_0, design.strands) + self.assertIn(expected_strand_1, design.strands) + + def test_add_full_crossover_on_extension_error(self) -> None: + """ + Before: + ↗ + / + / + / + 0 [------- [------> + + 1 <------] <------] + + Error: + ↗ + / + / + / + 0 [------+ +------> + | | + 1 <------+ +------] + """ + design: sc.Design = sc.Design( + helices=[sc.Helix(max_offset=100), sc.Helix(max_offset=100)] + ) + design.draw_strand(0, 0).to(8).extension_3p(5) + design.draw_strand(0, 8).to(16) + design.draw_strand(1, 8).to(0) + design.draw_strand(1, 16).to(8) + + with self.assertRaises(sc.IllegalDesignError): + design.add_full_crossover(0, 1, 8, True) + + def test_add_half_crossover_on_extension_ok(self) -> None: + """ + Before: + □ + \ + \ + 0 -------> + + 1 <------] + + After: + □ + \ + \ + 0 -------+ + | + 1 <------+ + """ + # Setup + design: sc.Design = sc.Design( + helices=[sc.Helix(max_offset=100), sc.Helix(max_offset=100)] + ) + design.draw_strand(0, 0).extension_5p(5).to(8) + design.draw_strand(1, 8).to(0) + + # Action + design.add_half_crossover(0, 1, 7, True) + + # Validation + expected_strand: sc.Strand = sc.Strand([ + sc.Extension(5), + sc.Domain(0, True, 0, 8), + sc.Domain(1, False, 0, 8) + ]) + self.assertEqual(1, len(design.strands)) + self.assertEqual(expected_strand, design.strands[0]) + + def test_add_half_crossover_on_extension_error(self) -> None: + """ + Before: + □ + \ + \ + 0 -------> + + 1 <------] + + Error: + □ + \ + \ + 0 +------> + | + 1 +------] + """ + # Setup + design: sc.Design = sc.Design( + helices=[sc.Helix(max_offset=100), sc.Helix(max_offset=100)] + ) + design.draw_strand(0, 0).extension_5p(5).to(8) + design.draw_strand(1, 8).to(0) + + with self.assertRaises(sc.IllegalDesignError): + design.add_half_crossover(0, 1, 0, True) + + def test_add_half_crossover_on_existing_crossover_should_error_5p_case(self) -> None: + """ + 0 +------] + | + 1 +------> + ^ + error to cross here + v + 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) + + with self.assertRaises(sc.IllegalDesignError): + design.add_half_crossover(1, 2, 0, True) + + def test_add_half_crossover_on_existing_crossover_should_error_3p_case(self) -> None: + """ + 0 <------+ + | + 1 [------+ + ^ + error to cross here + v + 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(1, 0).to(10).cross(0).to(0) + design.draw_strand(2, 10).to(0) + + with self.assertRaises(sc.IllegalDesignError): + design.add_half_crossover(1, 2, 9, True) + + def test_nick_on_extension(self) -> None: + """ + Before: + ↗ + / + / + / + 0 [------- + + After: + ↗ + / + / + / + 0 [-->[--- + """ + # Setup + design: sc.Design = sc.Design(helices=[sc.Helix(max_offset=100), sc.Helix(max_offset=100)]) + design.draw_strand(0, 0).to(8).extension_3p(5) + + # Nick + design.add_nick(0, 4, True) + + # Verification + expected_strand1: sc.Strand = sc.Strand([ + sc.Domain(0, True, 0, 4), + ]) + expected_strand2: sc.Strand = sc.Strand([ + sc.Domain(0, True, 4, 8), + sc.Extension(5) + ]) + self.assertEquals(2, len(design.strands)) + self.assertIn(expected_strand1, design.strands) + self.assertIn(expected_strand2, design.strands) + + +class TestAutocalculatedData(unittest.TestCase): + + def test_helix_min_max_offsets_illegal_explicitly_specified(self) -> None: + helices = [sc.Helix(min_offset=5, max_offset=5)] + with self.assertRaises(sc.IllegalDesignError): + sc.Design(helices=helices, strands=[], grid=sc.square) + + def test_helix_min_max_offsets_illegal_autocalculated(self) -> None: + helices = [sc.Helix(min_offset=5)] + ss = sc.Domain(0, True, 0, 4) + strand = sc.Strand([ss]) + with self.assertRaises(sc.IllegalDesignError): + sc.Design(helices=helices, strands=[strand], grid=sc.square) + + def test_helix_min_max_offsets(self) -> None: + helices = [sc.Helix(), sc.Helix(min_offset=-5), sc.Helix(max_offset=5), + sc.Helix(min_offset=5, max_offset=10)] + ss_0 = sc.Domain(helix=0, forward=True, start=20, end=25) + ss_1 = sc.Domain(helix=1, forward=False, start=-5, end=30) + ss_2 = sc.Domain(helix=2, forward=True, start=0, end=5) + ss_3 = sc.Domain(helix=3, forward=False, start=5, end=10) + strand = sc.Strand([ss_0, ss_1, ss_2, ss_3]) + design = sc.Design(helices=helices, strands=[strand], grid=sc.square) + self.assertEqual(0, design.helices[0].min_offset) + self.assertEqual(25, design.helices[0].max_offset) + self.assertEqual(-5, design.helices[1].min_offset) + self.assertEqual(30, design.helices[1].max_offset) + self.assertEqual(0, design.helices[2].min_offset) + self.assertEqual(5, design.helices[2].max_offset) + self.assertEqual(5, design.helices[3].min_offset) + self.assertEqual(10, design.helices[3].max_offset) + + def test_helix_max_offset(self) -> None: + helices = [sc.Helix(), sc.Helix(max_offset=8), sc.Helix()] + ss_0 = sc.Domain(helix=0, forward=True, start=5, end=10) + ss_1 = sc.Domain(helix=1, forward=False, start=2, end=6) + ss_2 = sc.Domain(helix=2, forward=True, start=0, end=5) + strand = sc.Strand([ss_0, ss_1, ss_2]) + design = sc.Design(helices=helices, strands=[strand], grid=sc.square) + self.assertEqual(10, design.helices[0].max_offset) + self.assertEqual(8, design.helices[1].max_offset) + self.assertEqual(5, design.helices[2].max_offset) + + +class TestSetHelixIdx(unittest.TestCase): + def test_set_helix_idx(self) -> None: + helices = [sc.Helix(max_offset=20), sc.Helix(max_offset=20), sc.Helix(max_offset=20)] + ss_0 = sc.Domain(helix=0, forward=True, start=0, end=6) + ss_1 = sc.Domain(helix=1, forward=True, start=0, end=7) + ss_2 = sc.Domain(helix=2, forward=True, start=0, end=8) + strand = sc.Strand([ss_0, ss_1, ss_2]) + design = sc.Design(helices=helices, strands=[strand], grid=sc.square) + design.set_helix_idx(2, 5) + design.set_helix_idx(1, 3) + design.set_helix_idx(0, 2) + self.assertEqual(3, len(design.helices)) + self.assertTrue(2 in design.helices) + self.assertTrue(3 in design.helices) + self.assertTrue(5 in design.helices) + h2 = design.helices[2] + h3 = design.helices[3] + h5 = design.helices[5] + self.assertEqual(2, h2.idx) + self.assertEqual(3, h3.idx) + self.assertEqual(5, h5.idx) + + ss2 = design.domain_at(2, 0, True) + ss3 = design.domain_at(3, 0, True) + ss5 = design.domain_at(5, 0, True) + self.assertEqual(2, ss2.helix) + self.assertEqual(3, ss3.helix) + self.assertEqual(5, ss5.helix) + self.assertEqual(6, ss2.end) + self.assertEqual(7, ss3.end) + self.assertEqual(8, ss5.end) + + +class TestDesignPitchYawRollOfHelix(unittest.TestCase): + def setUp(self) -> None: + n = 'north' + helix = sc.Helix(max_offset=12, group=n, grid_position=(1, 2), roll=4) + + group_north = sc.HelixGroup(position=sc.Position3D(x=0, y=-200, z=0), grid=sc.square, pitch=12, + yaw=40, roll=32) + self.design = sc.Design(helices=[helix], groups={n: group_north}, strands=[]) + self.helix = helix + + def test_design_pitch_of_helix(self) -> None: + self.assertEqual(12, self.design.pitch_of_helix(self.helix)) + + def test_design_yaw_of_helix(self) -> None: + self.assertEqual(40, self.design.yaw_of_helix(self.helix)) + + def test_design_roll_of_helix(self) -> None: + self.assertEqual(36, self.design.roll_of_helix(self.helix)) + + +class TestHelixGroups(unittest.TestCase): + def setUp(self) -> None: + n = 'north' + e = 'east' + s = 'south' + w = 'west' + helices = [ + sc.Helix(max_offset=20, group=n, grid_position=(1, 1)), # 0 + sc.Helix(max_offset=21, group=n, grid_position=(0, 1)), # 1 + sc.Helix(max_offset=19, group=n, grid_position=(0, 2)), # 2 + sc.Helix(max_offset=18, group=n, grid_position=(1, 2)), # 3 + sc.Helix(max_offset=17, group=n, grid_position=(2, 2)), # 4 + sc.Helix(max_offset=16, group=n, grid_position=(2, 1)), # 5 + sc.Helix(max_offset=24, group=s), # 6 + sc.Helix(max_offset=25, group=s), # 7 + sc.Helix(max_offset=26, group=w, position=sc.Position3D(x=0, y=0, z=0)), # 8 + sc.Helix(max_offset=27, group=w, position=sc.Position3D(x=0, y=2.5, z=0)), # 9 + sc.Helix(idx=13, max_offset=22, group=e), # 13 + sc.Helix(idx=15, max_offset=23, group=e), # 15 + ] + group_north = sc.HelixGroup(position=sc.Position3D(x=0, y=-200, z=0), grid=sc.honeycomb) + group_south = sc.HelixGroup(position=sc.Position3D(x=0, y=70, z=0), helices_view_order=[7, 6], + grid=sc.square) + group_east = sc.HelixGroup(position=sc.Position3D(x=0, y=0, z=100), pitch=45, grid=sc.square) + group_west = sc.HelixGroup() + groups = { + n: group_north, + e: group_east, + s: group_south, + w: group_west, + } + self.design = sc.Design(helices=helices, groups=groups, strands=[]) + self.n = n + self.e = e + self.s = s + self.w = w + + def test_helix_groups(self) -> None: + self._asserts_for_fixture(self.design) + + def test_helix_groups_to_from_JSON(self) -> None: + n = self.n + e = self.e + s = self.s + w = self.w + design_json_str = self.design.to_json() + + design_json_map = json.loads(design_json_str) + groups_map = design_json_map[sc.groups_key] + group_n = groups_map[n] + group_e = groups_map[e] + group_s = groups_map[s] + group_w = groups_map[w] + + pos_n = group_n[sc.position_key] + self.assertAlmostEqual(0, pos_n['x']) + self.assertAlmostEqual(-200, pos_n['y']) + self.assertAlmostEqual(0, pos_n['z']) + + pos_s = group_e[sc.position_key] + self.assertAlmostEqual(0, pos_s['x']) + self.assertAlmostEqual(0, pos_s['y']) + self.assertAlmostEqual(100, pos_s['z']) + + pos_w = group_s[sc.position_key] + self.assertAlmostEqual(0, pos_w['x']) + self.assertAlmostEqual(70, pos_w['y']) + self.assertAlmostEqual(0, pos_w['z']) + + pos_e = group_w[sc.position_key] + self.assertAlmostEqual(0, pos_e['x']) + self.assertAlmostEqual(0, pos_e['y']) + self.assertAlmostEqual(0, pos_e['z']) + + helices_map = design_json_map[sc.helices_key] + self.assertEqual(12, len(helices_map)) + helix0_map = helices_map[0] + helix1_map = helices_map[1] + helix2_map = helices_map[2] + helix3_map = helices_map[3] + helix4_map = helices_map[4] + helix5_map = helices_map[5] + helix6_map = helices_map[6] + helix7_map = helices_map[7] + helix8_map = helices_map[8] + helix9_map = helices_map[9] + helix13_map = helices_map[10] + helix15_map = helices_map[11] + + self.assertEqual(n, helix0_map[sc.group_key]) + self.assertEqual(n, helix1_map[sc.group_key]) + self.assertEqual(n, helix2_map[sc.group_key]) + self.assertEqual(n, helix3_map[sc.group_key]) + self.assertEqual(n, helix4_map[sc.group_key]) + self.assertEqual(n, helix5_map[sc.group_key]) + self.assertEqual(s, helix6_map[sc.group_key]) + self.assertEqual(s, helix7_map[sc.group_key]) + self.assertEqual(w, helix8_map[sc.group_key]) + self.assertEqual(w, helix9_map[sc.group_key]) + self.assertEqual(e, helix13_map[sc.group_key]) + self.assertEqual(e, helix15_map[sc.group_key]) + + self.assertEqual(0, helix0_map[sc.idx_on_helix_key]) + self.assertEqual(1, helix1_map[sc.idx_on_helix_key]) + self.assertEqual(2, helix2_map[sc.idx_on_helix_key]) + self.assertEqual(3, helix3_map[sc.idx_on_helix_key]) + self.assertEqual(4, helix4_map[sc.idx_on_helix_key]) + self.assertEqual(5, helix5_map[sc.idx_on_helix_key]) + self.assertEqual(6, helix6_map[sc.idx_on_helix_key]) + self.assertEqual(7, helix7_map[sc.idx_on_helix_key]) + self.assertEqual(8, helix8_map[sc.idx_on_helix_key]) + self.assertEqual(9, helix9_map[sc.idx_on_helix_key]) + self.assertEqual(13, helix13_map[sc.idx_on_helix_key]) + self.assertEqual(15, helix15_map[sc.idx_on_helix_key]) + + design_from_json = sc.Design.from_scadnano_json_str(design_json_str) + self._asserts_for_fixture(design_from_json) + + def test_helix_groups_fail_nonexistent(self) -> None: + helices = [ + sc.Helix(max_offset=20, group="north"), + sc.Helix(max_offset=21, group="east"), + ] + group_north = sc.HelixGroup(position=sc.Position3D(x=0, y=-200, z=0), grid=sc.honeycomb) + groups = {self.n: group_north} + with self.assertRaises(sc.IllegalDesignError): + sc.Design(helices=helices, groups=groups, strands=[]) + + def _asserts_for_fixture(self, design: sc.Design): + n = self.n + e = self.e + s = self.s + w = self.w + groups = design.groups + if groups is None: + return # this makes MyPy shut up about how groups might be None + + self.assertEqual(4, len(groups)) + + self.assertSequenceEqual([0, 1, 2, 3, 4, 5], groups[n].helices_view_order) + self.assertSequenceEqual([7, 6], groups[s].helices_view_order) + self.assertSequenceEqual([8, 9], groups[w].helices_view_order) + self.assertSequenceEqual([13, 15], groups[e].helices_view_order) + + self.assertEqual(sc.Grid.honeycomb, groups[n].grid) + self.assertEqual(sc.Grid.square, groups[e].grid) + self.assertEqual(sc.Grid.square, groups[s].grid) + self.assertEqual(sc.Grid.none, groups[w].grid) + + self.assertAlmostEqual(0, groups[n].pitch) + self.assertAlmostEqual(45, groups[e].pitch) + self.assertAlmostEqual(0, groups[s].pitch) + self.assertAlmostEqual(0, groups[w].pitch) + + # test auto-assignment of grid_positions based on helices view order + self.assertSequenceEqual([0, 1], design.helices[6].grid_position) + self.assertSequenceEqual([0, 0], design.helices[7].grid_position) + self.assertSequenceEqual([0, 0], design.helices[13].grid_position) + self.assertSequenceEqual([0, 1], design.helices[15].grid_position) + + def test_JSON_bad_uses_groups_and_top_level_grid(self) -> None: + json_str = ''' +{ + "grid": "none", + "groups": { + "north": { + "position": {"x": 0, "y": -200, "z": 0}, + "grid": "honeycomb" + }, + "east": { + "position": {"x": 0, "y": 0, "z": 100}, + "pitch": 45, + "grid": "square" + } + }, + "helices": [ + {"group": "north", "max_offset": 20, "grid_position": [1, 1]}, + {"group": "north", "max_offset": 21, "grid_position": [0, 1]}, + {"group": "east", "max_offset": 22, "grid_position": [0, 13]}, + {"group": "east", "max_offset": 23, "grid_position": [0, 15]} + ], + "strands": [ + { + "color": "#f74308", + "domains": [ + {"helix": 0, "forward": true, "start": 0, "end": 8}, + {"helix": 1, "forward": false, "start": 0, "end": 8} + ] + }, + { + "color": "#57bb00", + "domains": [ + {"helix": 2, "forward": true, "start": 0, "end": 8}, + {"helix": 3, "forward": false, "start": 0, "end": 8} + ] + } + ] +} + ''' + with self.assertRaises(sc.IllegalDesignError): + sc.Design.from_scadnano_json_str(json_str) + + def test_JSON_bad_uses_groups_and_top_level_helices_view_order(self) -> None: + json_str = ''' +{ + "helices_view_order": [3, 2, 1, 0], + "groups": { + "north": { + "position": {"x": 0, "y": -200, "z": 0}, + "grid": "honeycomb" + }, + "east": { + "position": {"x": 0, "y": 0, "z": 100}, + "pitch": 45, + "grid": "square" + } + }, + "helices": [ + {"group": "north", "max_offset": 20, "grid_position": [1, 1]}, + {"group": "north", "max_offset": 21, "grid_position": [0, 1]}, + {"group": "east", "max_offset": 22, "grid_position": [0, 13]}, + {"group": "east", "max_offset": 23, "grid_position": [0, 15]} + ], + "strands": [ + { + "color": "#f74308", + "domains": [ + {"helix": 0, "forward": true, "start": 0, "end": 8}, + {"helix": 1, "forward": false, "start": 0, "end": 8} + ] + }, + { + "color": "#57bb00", + "domains": [ + {"helix": 2, "forward": true, "start": 0, "end": 8}, + {"helix": 3, "forward": false, "start": 0, "end": 8} + ] + } + ] +} + ''' + with self.assertRaises(sc.IllegalDesignError): + sc.Design.from_scadnano_json_str(json_str) + + def test_JSON_bad_no_groups_but_helices_reference_groups(self) -> None: + json_str = ''' +{ + "grid": "square", + "helices": [ + {"group": "north", "max_offset": 20, "grid_position": [0, 0]}, + {"group": "north", "max_offset": 21, "grid_position": [0, 1]}, + {"group": "east", "max_offset": 22, "grid_position": [0, 2]}, + {"group": "east", "max_offset": 23, "grid_position": [0, 3]} + ], + "strands": [ + { + "color": "#f74308", + "domains": [ + {"helix": 0, "forward": true, "start": 0, "end": 8}, + {"helix": 1, "forward": false, "start": 0, "end": 8} + ] + }, + { + "color": "#57bb00", + "domains": [ + {"helix": 2, "forward": true, "start": 0, "end": 8}, + {"helix": 3, "forward": false, "start": 0, "end": 8} + ] + } + ] +} + +''' + with self.assertRaises(sc.IllegalDesignError): + sc.Design.from_scadnano_json_str(json_str) + + +class TestNames(unittest.TestCase): + + def test_strand_domain_names_json(self) -> None: + helices = [sc.Helix(max_offset=100), sc.Helix(max_offset=100), sc.Helix(max_offset=100)] + strand0 = sc.Strand(name='strand0', domains=[ + sc.Domain(0, True, 0, 8, name='domain_forward0'), + sc.Domain(0, True, 8, 16, name='domain_forward1'), + ]) + strand1 = sc.Strand(name='strand1', domains=[ + sc.Domain(0, False, 0, 8, name='domain_reverse0'), + sc.Loopout(3, name='loopout'), + sc.Domain(2, True, 0, 8, name='domain_forward2'), + ]) + strand2 = sc.Strand(domains=[ + sc.Domain(1, True, 0, 8), + sc.Domain(1, True, 8, 16, name='domain_forward0'), + ]) + strands = [strand0, strand1, strand2] + design = sc.Design(helices=helices, strands=strands, grid=sc.square) + + json_map = design.to_json_serializable(suppress_indent=False) + + self.assertIn(sc.strand_name_key, json_map[sc.strands_key][0].keys()) + self.assertIn(sc.strand_name_key, json_map[sc.strands_key][1].keys()) + self.assertNotIn(sc.strand_name_key, json_map[sc.strands_key][2].keys()) + self.assertEqual('strand0', json_map[sc.strands_key][0][sc.strand_name_key]) + self.assertEqual('strand1', json_map[sc.strands_key][1][sc.strand_name_key]) + + self.assertIn(sc.domain_name_key, json_map[sc.strands_key][0]['domains'][0]) + self.assertIn(sc.domain_name_key, json_map[sc.strands_key][0]['domains'][1]) + self.assertEqual('domain_forward0', json_map[sc.strands_key][0]['domains'][0][sc.domain_name_key]) + self.assertEqual('domain_forward1', json_map[sc.strands_key][0]['domains'][1][sc.domain_name_key]) + + self.assertIn(sc.domain_name_key, json_map[sc.strands_key][1]['domains'][0]) + self.assertIn(sc.domain_name_key, json_map[sc.strands_key][1]['domains'][1]) + self.assertIn(sc.domain_name_key, json_map[sc.strands_key][1]['domains'][2]) + self.assertEqual('domain_reverse0', json_map[sc.strands_key][1]['domains'][0][sc.domain_name_key]) + self.assertEqual('loopout', json_map[sc.strands_key][1]['domains'][1][sc.domain_name_key]) + self.assertEqual('domain_forward2', json_map[sc.strands_key][1]['domains'][2][sc.domain_name_key]) + + self.assertIn(sc.domain_name_key, json_map[sc.strands_key][2]['domains'][1]) + self.assertEqual('domain_forward0', json_map[sc.strands_key][2]['domains'][1][sc.domain_name_key]) + + design_from_json = sc.Design.from_scadnano_json_map(json_map) + + self.assertEqual(3, len(design_from_json.strands)) + self.assertEqual('strand0', design_from_json.strands[0].name) + self.assertEqual('strand1', design_from_json.strands[1].name) + self.assertEqual(None, design_from_json.strands[2].name) + + self.assertEqual('domain_forward0', design_from_json.strands[0].domains[0].name) + self.assertEqual('domain_forward1', design_from_json.strands[0].domains[1].name) + + self.assertEqual('domain_reverse0', design_from_json.strands[1].domains[0].name) + self.assertEqual('loopout', design_from_json.strands[1].domains[1].name) + self.assertEqual('domain_forward2', design_from_json.strands[1].domains[2].name) + + self.assertEqual(None, design_from_json.strands[2].domains[0].name) + self.assertEqual('domain_forward0', design_from_json.strands[2].domains[1].name) + + def test_strand_names_can_be_nonunique(self) -> None: + helices = [sc.Helix(max_offset=100)] + strand0 = sc.Strand(name='strand0', domains=[ + sc.Domain(0, True, 0, 8, name='domain_forward0'), + sc.Domain(0, True, 8, 16, name='domain_forward1'), + ]) + strand1 = sc.Strand(name='strand0', domains=[ + sc.Domain(0, False, 0, 8, name='domain_reverse0'), + sc.Domain(0, False, 8, 16, name='domain_reverse1'), + ]) + strands = [strand0, strand1] + sc.Design(helices=helices, strands=strands, grid=sc.square) + + +class TestJSON(unittest.TestCase): + + def test_grid_design_level_converted_to_enum_from_string(self) -> None: + # reproduces an error where the grid was stored as a string instead of the Grid enum type + json_str = ''' +{ + "version": "0.14.0", + "grid": "square", + "helices": [ + {"grid_position": [0, 0]}, + {"grid_position": [0, 1]} + ], + "strands": [] +} + ''' + design = sc.Design.from_scadnano_json_str(json_str) + grid = design.grid + self.assertTrue(type(grid) is sc.Grid) + self.assertEqual(sc.Grid.square, grid) + + def test_grid_helix_group_level_converted_to_enum_from_string(self) -> None: + # reproduces an error where the grid was stored as a string instead of the Grid enum type + json_str = ''' +{ + "version": "0.15.0", + "groups": { + "north": { + "position": {"x": 0, "y": -10, "z": 0}, + "grid": "honeycomb" + }, + "east": { + "position": {"x": 0, "y": 0, "z": 10}, + "grid": "square" + }, + "south": { + "position": {"x": 0, "y": 10, "z": 0}, + "grid": "hex" + }, + "west": { + "position": {"x": 0, "y": 0, "z": -10}, + "grid": "none" + } + }, + "helices": [ + {"group": "north", "max_offset": 20, "grid_position": [0, 0], "idx": 0}, + {"group": "north", "max_offset": 21, "grid_position": [1, 0], "idx": 1}, + {"group": "north", "max_offset": 19, "grid_position": [1, 1], "idx": 2}, + {"group": "north", "max_offset": 18, "grid_position": [0, 1], "idx": 3}, + {"group": "north", "max_offset": 17, "grid_position": [-1, 1], "idx": 4}, + {"group": "north", "max_offset": 16, "grid_position": [-1, 0], "idx": 5}, + {"group": "south", "max_offset": 24, "grid_position": [0, 1], "idx": 6}, + {"group": "south", "max_offset": 25, "grid_position": [0, 0], "idx": 7}, + {"group": "west", "max_offset": 26, "position": {"x": 0, "y": 0, "z": 0}, "idx": 8}, + {"group": "west", "max_offset": 27, "position": {"x": 0, "y": 3, "z": 0}, "idx": 9}, + {"group": "east", "max_offset": 22, "grid_position": [0, 0], "idx": 13}, + {"group": "east", "max_offset": 23, "grid_position": [0, 1], "idx": 15} + ], + "strands": [] +} + ''' + design = sc.Design.from_scadnano_json_str(json_str) + + grid_n = design.groups['north'].grid + grid_e = design.groups['east'].grid + grid_s = design.groups['south'].grid + grid_w = design.groups['west'].grid + for grid in [grid_n, grid_e, grid_s, grid_w]: + self.assertTrue(type(grid) is sc.Grid) + + self.assertEqual(sc.Grid.honeycomb, grid_n) + self.assertEqual(sc.Grid.square, grid_e) + self.assertEqual(sc.Grid.hex, grid_s) + self.assertEqual(sc.Grid.none, grid_w) + + def test_legacy_idt_name_import__no_strand_name(self) -> None: + # tests proper importing of old format when name was a subfield of idt; + # ensures if that exists and no Strand.name field exists, the idt.name is used as Strand.name + json_str = ''' + { + "version": "0.14.0", + "grid": "square", + "helices": [ + {"grid_position": [0, 0]}, + {"grid_position": [0, 1]} + ], + "strands": [ + { + "color": "#f74308", + "sequence": "TATTATAGTCTTACCCTGAC", + "idt": {"name": "staple1", "scale": "100nm", "purification": "HPLC", "plate": "plate1", "well": "A1"}, + "domains": [ + {"helix": 0, "forward": true, "start": 0, "end": 10}, + {"helix": 1, "forward": false, "start": 0, "end": 10} + ] + } + ] + } + ''' + design = sc.Design.from_scadnano_json_str(json_str) + self.assertEqual(1, len(design.strands)) + strand = design.strands[0] + self.assertEqual('staple1', strand.name) + self.assertEqual('100nm', strand.idt.scale) + self.assertEqual('HPLC', strand.idt.purification) + self.assertEqual('plate1', strand.idt.plate) + self.assertEqual('A1', strand.idt.well) + + def test_legacy_idt_name_import__strand_name_exists(self) -> None: + # tests proper importing of old format when name was a subfield of idt; + # ensures if both exist, we use Strand.name + json_str = ''' + { + "version": "0.14.0", + "grid": "square", + "helices": [ + {"grid_position": [0, 0]}, + {"grid_position": [0, 1]} + ], + "strands": [ + { + "name": "staple1 strand level", + "color": "#f74308", + "sequence": "TATTATAGTCTTACCCTGAC", + "idt": {"name": "staple1", "scale": "100nm", "purification": "HPLC", "plate": "plate1", "well": "A1"}, + "domains": [ + {"helix": 0, "forward": true, "start": 0, "end": 10}, + {"helix": 1, "forward": false, "start": 0, "end": 10} + ] + } + ] + } + ''' + design = sc.Design.from_scadnano_json_str(json_str) + self.assertEqual(1, len(design.strands)) + strand = design.strands[0] + self.assertEqual('staple1 strand level', strand.name) + + def test_Helix_major_tick_start_default_min_offset(self) -> None: + helices = [ + sc.Helix(min_offset=10, max_offset=100), + sc.Helix(max_offset=100), + sc.Helix(major_tick_start=15), + ] + design = sc.Design(helices=helices, strands=[], grid=sc.square) + self.assertEqual(10, design.helices[0].major_tick_start) + self.assertEqual(0, design.helices[1].major_tick_start) + self.assertEqual(15, design.helices[2].major_tick_start) + + design_json_map = design.to_json_serializable(suppress_indent=False) + self.assertNotIn(sc.major_tick_start_key, design_json_map['helices'][0]) + self.assertNotIn(sc.major_tick_start_key, design_json_map['helices'][1]) + self.assertIn(sc.major_tick_start_key, design_json_map['helices'][2]) + self.assertEqual(15, design_json_map['helices'][2][sc.major_tick_start_key]) + + # this isn't related to major_tick_start, but it failed for some reason so let's check it + self.assertIn(sc.grid_position_key, design_json_map['helices'][0]) + self.assertIn(sc.grid_position_key, design_json_map['helices'][1]) + self.assertIn(sc.grid_position_key, design_json_map['helices'][2]) + self.assertSequenceEqual([0, 0], design_json_map['helices'][0][sc.grid_position_key]) + self.assertSequenceEqual([0, 1], design_json_map['helices'][1][sc.grid_position_key]) + self.assertSequenceEqual([0, 2], design_json_map['helices'][2][sc.grid_position_key]) + + design_json_str = json.dumps(design_json_map) + design = sc.Design.from_scadnano_json_str(design_json_str) + self.assertEqual(10, design.helices[0].major_tick_start) + self.assertEqual(0, design.helices[1].major_tick_start) + self.assertEqual(15, design.helices[2].major_tick_start) + + def test_Helix_major_tick_periodic_distances(self) -> None: + grid = sc.square + helices = [ + sc.Helix(major_tick_start=10, max_offset=30, major_tick_distance=5), + sc.Helix(major_tick_start=10, max_offset=30, major_tick_periodic_distances=[2, 3]), + sc.Helix(major_tick_start=10, max_offset=30, major_ticks=[10, 20, 30]), + sc.Helix(major_tick_start=10, max_offset=30), + sc.Helix(max_offset=30), + ] + design = sc.Design(helices=helices, strands=[], grid=grid) + self.assertEqual(10, design.helices[0].major_tick_start) + + self.assertSequenceEqual([10, 15, 20, 25, 30], design.helices[0].calculate_major_ticks(grid)) + self.assertSequenceEqual([10, 12, 15, 17, 20, 22, 25, 27, 30], + design.helices[1].calculate_major_ticks(grid)) + self.assertSequenceEqual([10, 20, 30], design.helices[2].calculate_major_ticks(grid)) + self.assertSequenceEqual([10, 18, 26], design.helices[3].calculate_major_ticks(grid)) + self.assertSequenceEqual([0, 8, 16, 24], design.helices[4].calculate_major_ticks(grid)) + + design_json_map = design.to_json_serializable(suppress_indent=False) + + h0 = design_json_map['helices'][0] + self.assertNotIn(sc.major_ticks_key, h0) + self.assertNotIn(sc.major_tick_periodic_distances_key, h0) + self.assertIn(sc.major_tick_distance_key, h0) + self.assertEqual(5, h0[sc.major_tick_distance_key]) + + h1 = design_json_map['helices'][1] + self.assertNotIn(sc.major_ticks_key, h1) + self.assertIn(sc.major_tick_periodic_distances_key, h1) + self.assertNotIn(sc.major_tick_distance_key, h1) + self.assertSequenceEqual([2, 3], h1[sc.major_tick_periodic_distances_key]) + + h2 = design_json_map['helices'][2] + self.assertIn(sc.major_ticks_key, h2) + self.assertNotIn(sc.major_tick_distance_key, h2) + self.assertNotIn(sc.major_tick_periodic_distances_key, h2) + self.assertSequenceEqual([10, 20, 30], h2[sc.major_ticks_key]) + + h3 = design_json_map['helices'][3] + self.assertNotIn(sc.major_ticks_key, h3) + self.assertNotIn(sc.major_tick_distance_key, h3) + self.assertNotIn(sc.major_tick_periodic_distances_key, h3) + self.assertIn(sc.major_tick_start_key, h3) + + h4 = design_json_map['helices'][4] + self.assertNotIn(sc.major_ticks_key, h4) + self.assertNotIn(sc.major_tick_distance_key, h4) + self.assertNotIn(sc.major_tick_periodic_distances_key, h4) + self.assertNotIn(sc.major_tick_start_key, h4) + + def test_default_helices_view_order_with_nondefault_helix_idxs_in_default_order(self) -> None: + helices = [sc.Helix(idx=1, max_offset=100), sc.Helix(idx=3, max_offset=100)] + design = sc.Design(helices=helices, strands=[]) + self.assertListEqual([1, 3], design.helices_view_order) + + # [1, 3] is default so json should not contain key + design_json_ser = design.to_json_serializable(suppress_indent=False) + self.assertFalse(sc.helices_view_order_key in design_json_ser) + + def test_default_helices_view_order_with_nondefault_helix_idxs_in_nondefault_order(self) -> None: + helices = [sc.Helix(idx=1, max_offset=100), sc.Helix(idx=3, max_offset=100)] + design = sc.Design(helices=helices, strands=[], helices_view_order=[3, 1]) + self.assertListEqual([3, 1], design.helices_view_order) + + # [1, 3] is default so json should not contain key + design_json_ser = design.to_json_serializable(suppress_indent=False) + actual_view_order = design_json_ser[sc.helices_view_order_key] + self.assertListEqual([3, 1], actual_view_order) + + def test_strand_labels(self) -> None: + helices = [sc.Helix(max_offset=100), sc.Helix(max_offset=100)] + strand0_expected = sc.Strand([sc.Domain(0, True, 0, 10)], label={ + 'name': 'strand 0', + 'num_domains': 1, + }) + strand1_expected = sc.Strand([sc.Domain(0, False, 0, 10), sc.Domain(1, True, 0, 10)], label={ + 'name': 'strand 1', + 'num_domains': 2, + }) + strands = [strand0_expected, strand1_expected] + design = sc.Design(helices=helices, strands=strands, grid=sc.square) + json_str = design.to_json() + design_from_json = sc.Design.from_scadnano_json_str(json_str) + strand0 = design_from_json.strands[0] + strand1 = design_from_json.strands[1] + self.assertDictEqual(strand0_expected.label, strand0.label) + self.assertDictEqual(strand1_expected.label, strand1.label) + + def test_strand_idt(self) -> None: + helices = [sc.Helix(max_offset=100), sc.Helix(max_offset=100)] + idt = sc.IDTFields(scale='25nm', purification='HPLC', plate='plate1', well='A2') + strand0_expected = sc.Strand([sc.Domain(0, True, 0, 10)], name='strand1', idt=idt) + strands = [strand0_expected] + design = sc.Design(helices=helices, strands=strands, grid=sc.square) + json_str = design.to_json() + design_from_json = sc.Design.from_scadnano_json_str(json_str) + strand0 = design_from_json.strands[0] + self.assertEqual(strand0_expected.idt, strand0.idt) + self.assertEqual('strand1', strand0.idt_export_name()) + self.assertEqual('25nm', strand0.idt.scale) + self.assertEqual('HPLC', strand0.idt.purification) + self.assertEqual('plate1', strand0.idt.plate) + self.assertEqual('A2', strand0.idt.well) + + def test_domain_labels(self) -> None: + helices = [sc.Helix(max_offset=100), sc.Helix(max_offset=100)] + dom00_expected = sc.Domain(0, True, 0, 10, label='domain 00') + dom10_expected = sc.Domain(0, False, 0, 10) + dom11_expected = sc.Domain(1, True, 0, 10, label='domain 11') + strand0 = sc.Strand([dom00_expected]) + strand1 = sc.Strand([dom10_expected, dom11_expected]) + strands = [strand0, strand1] + design = sc.Design(helices=helices, strands=strands, grid=sc.square) + json_str = design.to_json() + design_from_json = sc.Design.from_scadnano_json_str(json_str) + dom00 = design_from_json.strands[0].domains[0] + dom10 = design_from_json.strands[1].domains[0] + dom11 = design_from_json.strands[1].domains[1] + self.assertEqual(dom00_expected.label, dom00.label) + self.assertIsNone(dom10.label) + self.assertEqual(dom11_expected.label, dom11.label) + + def test_nondefault_geometry(self) -> None: + geometry_expected = sc.Geometry(rise_per_base_pair=10.0, helix_radius=4.0, bases_per_turn=11.0, + minor_groove_angle=10.0, + inter_helix_gap=5.0) + design = sc.Design(helices=[], strands=[], geometry=geometry_expected) + json_str = design.to_json() + design_from_json = sc.Design.from_scadnano_json_str(json_str) + geometry_actual = design_from_json.geometry + self.assertAlmostEqual(geometry_expected.rise_per_base_pair, geometry_actual.rise_per_base_pair) + self.assertAlmostEqual(geometry_expected.helix_radius, geometry_actual.helix_radius) + self.assertAlmostEqual(geometry_expected.bases_per_turn, geometry_actual.bases_per_turn) + self.assertAlmostEqual(geometry_expected.minor_groove_angle, geometry_actual.minor_groove_angle) + self.assertAlmostEqual(geometry_expected.inter_helix_gap, geometry_actual.inter_helix_gap) + + def test_nondefault_geometry_some_default(self) -> None: + geometry_expected = sc.Geometry(rise_per_base_pair=10.0, minor_groove_angle=10.0, inter_helix_gap=5.0) + design = sc.Design(helices=[], strands=[], geometry=geometry_expected) + json_str = design.to_json() + design_from_json = sc.Design.from_scadnano_json_str(json_str) + geometry_actual = design_from_json.geometry + self.assertAlmostEqual(geometry_expected.rise_per_base_pair, geometry_actual.rise_per_base_pair) + self.assertAlmostEqual(geometry_expected.helix_radius, geometry_actual.helix_radius) + self.assertAlmostEqual(geometry_expected.bases_per_turn, geometry_actual.bases_per_turn) + self.assertAlmostEqual(geometry_expected.minor_groove_angle, geometry_actual.minor_groove_angle) + self.assertAlmostEqual(geometry_expected.inter_helix_gap, geometry_actual.inter_helix_gap) + + def test_lack_of_NoIndent_on_helix_if_position_or_major_ticks_present(self) -> None: + helices = [sc.Helix(position=sc.Position3D(0, 0, 0))] + strands = [] + design = sc.Design(helices=helices, strands=strands) + json_map = design.to_json_serializable(suppress_indent=True) + helix_json = json_map[sc.helices_key][0] + self.assertTrue(isinstance(helix_json, sc.NoIndent)) + # self.assertTrue(isinstance(helix_json[sc.position_key], sc.NoIndent)) + + def test_NoIndent_on_helix_without_position_or_major_ticks_present(self) -> None: + helices = [sc.Helix()] + strands = [] + # will only be NoIndent if we don't have a Helix.position field, so make sure one isn't created + # by choosing a grid + design = sc.Design(helices=helices, strands=strands, grid=sc.square) + json_map = design.to_json_serializable(suppress_indent=True) + helix_json = json_map[sc.helices_key][0] + self.assertTrue(isinstance(helix_json, sc.NoIndent)) + + def test_error_when_grid_missing(self) -> None: + json_str = """ + { + "helices": [{"grid_position": [0,0]}], + "strands": [ + { + "color": 26316, + "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] + } + ] + } + """ + with self.assertRaises(sc.IllegalDesignError) as ex: + sc.Design.from_scadnano_json_str(json_str) + msg = ex.exception.args[0] + self.assertTrue('grid' in msg) + + def test_error_when_domain_helix_missing(self) -> None: + json_str = """ + { + "grid": "square", + "helices": [{"grid_position": [0,0]}], + "strands": [ + { + "color": 26316, + "domains": [ {"forward": true, "start": 0, "end": 32} ] + } + ] + } + """ + with self.assertRaises(sc.IllegalDesignError) as ex: + sc.Design.from_scadnano_json_str(json_str) + msg = ex.exception.args[0] + self.assertTrue('helix' in msg) + + def test_error_when_domain_forward_and_right_missing(self) -> None: + json_str = """ + { + "grid": "square", + "helices": [{"grid_position": [0,0]}], + "strands": [ + { + "color": 26316, + "domains": [ {"helix": 0, "start": 0, "end": 32} ] + } + ] + } + """ + with self.assertRaises(sc.IllegalDesignError) as ex: + sc.Design.from_scadnano_json_str(json_str) + msg = ex.exception.args[0] + self.assertTrue('forward' in msg) + self.assertTrue('right' in msg) + + def test_error_when_domain_start_missing(self) -> None: + json_str = """ + { + "grid": "square", + "helices": [{"grid_position": [0,0]}], + "strands": [ + { + "color": 26316, + "domains": [ {"helix": 0, "forward": true, "end": 32} ] + } + ] + } + """ + with self.assertRaises(sc.IllegalDesignError) as ex: + sc.Design.from_scadnano_json_str(json_str) + msg = ex.exception.args[0] + self.assertTrue('start' in msg) + + def test_error_when_domain_end_missing(self) -> None: + json_str = """ + { + "grid": "square", + "helices": [{"grid_position": [0,0]}], + "strands": [ + { + "color": 26316, + "domains": [ {"helix": 0, "forward": true, "start": 0 } ] + } + ] + } + """ + with self.assertRaises(sc.IllegalDesignError) as ex: + sc.Design.from_scadnano_json_str(json_str) + msg = ex.exception.args[0] + self.assertTrue('end' in msg) + + def test_error_when_strands_missing(self) -> None: + json_str = """ + { + "grid": "square", + "helices": [{"grid_position": [0,0]}] + } + """ + with self.assertRaises(sc.IllegalDesignError) as ex: + sc.Design.from_scadnano_json_str(json_str) + msg = ex.exception.args[0] + self.assertTrue('strands' in msg) + + def test_legacy_right_key(self) -> None: + json_str = """ + { + "grid": "square", + "helices": [{"grid_position": [0,0]}], + "strands": [ + { + "color": 26316, + "domains": [ {"helix": 0, "right": true, "start": 0, "end": 5 } ] + } + ] + } + """ + d = sc.Design.from_scadnano_json_str(json_str) + self.assertEqual(True, d.strands[0].domains[0].forward) + + def test_legacy_dna_sequence_key(self) -> None: + json_str = """ + { + "grid": "square", + "helices": [{"grid_position": [0,0]}], + "strands": [ + { + "color": 26316, + "dna_sequence": "ACGTA", + "domains": [ {"helix": 0, "right": true, "start": 0, "end": 5 } ] + } + ] + } + """ + d = sc.Design.from_scadnano_json_str(json_str) + self.assertEqual("ACGTA", d.strands[0].dna_sequence) + + def test_legacy_substrands_key(self) -> None: + json_str = """ + { + "grid": "square", + "helices": [{"grid_position": [0,0]}], + "strands": [ + { + "color": 26316, + "substrands": [ {"helix": 0, "forward": true, "start": 0, "end": 5 } ] + } + ] + } + """ + d = sc.Design.from_scadnano_json_str(json_str) + self.assertEqual(0, d.strands[0].domains[0].helix) + self.assertEqual(True, d.strands[0].domains[0].forward) + self.assertEqual(0, d.strands[0].domains[0].start) + self.assertEqual(5, d.strands[0].domains[0].end) + + def test_color_specified_with_integer(self) -> None: + # addresses https://github.com/UC-Davis-molecular-computing/scadnano-python-package/issues/58 + # 0066cc hex is 26316 decimal + json_str = """ + { + "grid": "square", + "helices": [{"grid_position": [0,0]}], + "strands": [ + { + "color": 26316, + "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] + } + ] + } + """ + d = sc.Design.from_scadnano_json_str(json_str) + expected_color_hex = '#0066cc' + actual_color_hex = d.strands[0].color.to_json_serializable(False) + self.assertEqual(expected_color_hex, actual_color_hex) + + def test_single_helix_group_and_helices_specify_pitch_and_yaw(self) -> None: + json_str = """ + { + "helices": [ + { + "group": "north", + "position": {"x": 1, "y": 2, "z": 3}, + "pitch": 4, + "roll": 5, + "yaw": 6 + }, + { + "group": "north", + "position": {"x": 3, "y": 2, "z": 3}, + "roll": 15 + } + ], + "groups": { + "north": { + "position": {"x": 0, "y": -200, "z": 0}, + "pitch": 21, + "yaw": 13, + "grid": "none" + } + }, + "strands": [ + { + "color": "#0066cc", + "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] + } + ] + } + """ + d = sc.Design.from_scadnano_json_str(json_str) + helix0 = d.helices[0] + helix1 = d.helices[1] + self.assertEqual(sc.Position3D(1, 2, 3), helix0.position) + self.assertEqual(5, helix0.roll) + # Helix 0 should have been moved to a new helix group + pitch_25_yaw_19_group_name = f'pitch_25_yaw_19' + pitch_25_yaw_19_group = d.groups[pitch_25_yaw_19_group_name] + self.assertEqual(25, pitch_25_yaw_19_group.pitch) + self.assertEqual(19, pitch_25_yaw_19_group.yaw) + self.assertEqual(pitch_25_yaw_19_group_name, helix0.group) + self.assertEqual(sc.Position3D(3, 2, 3), helix1.position) + self.assertEqual(15, helix1.roll) + self.assertEqual("north", helix1.group) + self.assertEqual(2, len(d.groups)) + + def test_only_individual_helices_specify_pitch_and_yaw(self) -> None: + json_str = """ + { + "helices": [ + { + "group": "north", + "position": {"x": 1, "y": 2, "z": 3}, + "pitch": 25, + "yaw": 19, + "roll": 5 + }, + { + "group": "north", + "position": {"x": 3, "y": 2, "z": 3}, + "pitch": 21, + "yaw": 13, + "roll": 15 + } + ], + "groups": { + "north": { + "position": {"x": 0, "y": -200, "z": 0}, + "grid": "none" + } + }, + "strands": [ + { + "color": "#0066cc", + "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] + } + ] + } + """ + d = sc.Design.from_scadnano_json_str(json_str) + helix0 = d.helices[0] + helix1 = d.helices[1] + + # Helix 0 should have been moved to a new helix group + pitch_25_yaw_19_group_name = f'pitch_25.0_yaw_19.0' + pitch_25_yaw_19_group = d.groups[pitch_25_yaw_19_group_name] + self.assertEqual(sc.Position3D(1, 2, 3), helix0.position) + self.assertEqual(25, pitch_25_yaw_19_group.pitch) + self.assertEqual(19, pitch_25_yaw_19_group.yaw) + self.assertEqual(5, helix0.roll) + self.assertEqual(pitch_25_yaw_19_group_name, helix0.group) + + # Helix 1 should have been moved to a new helix group + pitch_21_yaw_13_group_name = f'pitch_21.0_yaw_13.0' + pitch_21_yaw_13_group = d.groups[pitch_21_yaw_13_group_name] + self.assertEqual(sc.Position3D(3, 2, 3), helix1.position) + self.assertEqual(21, pitch_21_yaw_13_group.pitch) + self.assertEqual(13, pitch_21_yaw_13_group.yaw) + self.assertEqual(15, helix1.roll) + self.assertEqual(pitch_21_yaw_13_group_name, helix1.group) + + self.assertEqual(3, len(d.groups)) + + def test_only_helix_groups_specify_pitch_and_yaw(self) -> None: + json_str = """ + { + "helices": [ + { + "group": "north", + "position": {"x": 1, "y": 2, "z": 3}, + "roll": 5 + }, + { + "group": "south", + "position": {"x": 3, "y": 2, "z": 3}, + "roll": 15 + } + ], + "groups": { + "north": { + "position": {"x": 0, "y": -200, "z": 0}, + "pitch": 21, + "yaw": 13, + "grid": "none" + }, + "south": { + "position": {"x": 0, "y": -400, "z": 0}, + "pitch": 23, + "yaw": 98, + "grid": "none" + } + }, + "strands": [ + { + "color": "#0066cc", + "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] + } + ] + } + """ + d = sc.Design.from_scadnano_json_str(json_str) + helix0 = d.helices[0] + helix1 = d.helices[1] + + north_str = 'north' + south_str = 'south' + north_group = d.groups[north_str] + south_group = d.groups[south_str] + self.assertEqual(2, len(d.groups)) + + self.assertEqual(sc.Position3D(1, 2, 3), helix0.position) + self.assertEqual(5, helix0.roll) + self.assertEqual(21, north_group.pitch) + self.assertEqual(13, north_group.yaw) + self.assertEqual(north_str, helix0.group) + + self.assertEqual(sc.Position3D(3, 2, 3), helix1.position) + self.assertEqual(15, helix1.roll) + self.assertEqual(23, south_group.pitch) + self.assertEqual(98, south_group.yaw) + self.assertEqual(south_str, helix1.group) + + def test_both_helix_groups_and_helices_do_not_specify_pitch_nor_yaw(self) -> None: + json_str = """ + { + "helices": [ + { + "group": "north", + "position": {"x": 1, "y": 2, "z": 3}, + "roll": 5 + }, + { + "group": "south", + "position": {"x": 3, "y": 2, "z": 3}, + "roll": 15 + } + ], + "groups": { + "north": { + "position": {"x": 0, "y": -200, "z": 0}, + "grid": "none" + }, + "south": { + "position": {"x": 0, "y": -400, "z": 0}, + "grid": "none" + } + }, + "strands": [ + { + "color": "#0066cc", + "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] + } + ] + } + """ + d = sc.Design.from_scadnano_json_str(json_str) + helix0 = d.helices[0] + helix1 = d.helices[1] + + north_str = 'north' + south_str = 'south' + north_group = d.groups[north_str] + south_group = d.groups[south_str] + self.assertEqual(2, len(d.groups)) + + self.assertEqual(sc.Position3D(1, 2, 3), helix0.position) + self.assertEqual(5, helix0.roll) + self.assertEqual(0, north_group.pitch) + self.assertEqual(0, north_group.yaw) + self.assertEqual(north_str, helix0.group) + + self.assertEqual(sc.Position3D(3, 2, 3), helix1.position) + self.assertEqual(15, helix1.roll) + self.assertEqual(0, south_group.pitch) + self.assertEqual(0, south_group.yaw) + self.assertEqual(south_str, helix1.group) + + def test_multiple_helix_groups_helices_specify_pitch_and_yaw(self) -> None: + json_str = """ + { + "helices": [ + { + "group": "north", + "position": {"x": 1, "y": 2, "z": 3}, + "pitch": 4, + "roll": 5, + "yaw": 6 }, + { + "group": "south", + "position": {"x": 3, "y": 2, "z": 3}, + "roll": 15 + } + ], + "groups": { + "north": { + "position": {"x": 0, "y": -200, "z": 0}, + "pitch": 21, + "yaw": 13, + "grid": "none" + }, + "south": { + "position": {"x": 0, "y": -400, "z": 0}, + "pitch": 23, + "yaw": 98, + "grid": "none" + } + }, + "strands": [ + { + "color": "#0066cc", + "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] + } + ] + } + """ + d = sc.Design.from_scadnano_json_str(json_str) + helix0 = d.helices[0] + helix1 = d.helices[1] + + north_str = 'north' + south_str = 'south' + north_group = d.groups[north_str] + south_group = d.groups[south_str] + self.assertEqual(2, len(d.groups)) + + self.assertEqual(sc.Position3D(1, 2, 3), helix0.position) + self.assertEqual(5, helix0.roll) + self.assertEqual(25, north_group.pitch) + self.assertEqual(19, north_group.yaw) + self.assertEqual(north_str, helix0.group) + + self.assertEqual(sc.Position3D(3, 2, 3), helix1.position) + self.assertEqual(15, helix1.roll) + self.assertEqual(23, south_group.pitch) + self.assertEqual(98, south_group.yaw) + self.assertEqual(south_str, helix1.group) + + def test_multiple_helix_groups_helices_specify_pitch_and_yaw_invalid(self) -> None: + json_str = """ + { + "helices": [ + { + "group": "north", + "position": {"x": 1, "y": 2, "z": 3}, + "pitch": 4, + "roll": 5, + "yaw": 6 + }, + { + "group": "north", + "position": {"x": 3, "y": 2, "z": 3}, + "pitch": 10, + "roll": 5, + "yaw": 84 + } + ], + "groups": { + "north": { + "position": {"x": 0, "y": -200, "z": 0}, + "grid": "none" + }, + "east": { + "position": {"x": 0, "y": 0, "z": 100}, + "pitch": 45, + "grid": "square" + } + }, + "strands": [ + { + "color": "#0066cc", + "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] + } + ] + } + """ + # Should fail because multiple helices in same helix group are non-parallel + with self.assertRaises(sc.IllegalDesignError): + sc.Design.from_scadnano_json_str(json_str) + + def test_position_specified_with_origin_keyword(self) -> None: + # addresses https://github.com/UC-Davis-molecular-computing/scadnano-python-package/issues/59 + json_str = """ + { + "grid": "none", + "helices": [{ + "origin": {"x": 1, "y": 2, "z": 3}, + "pitch": 4, + "roll": 5, + "yaw": 6 + }], + "strands": [ + { + "color": "#0066cc", + "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ] + } + ] + } + """ + d = sc.Design.from_scadnano_json_str(json_str) + expected_position = sc.Position3D(1, 2, 3) + expected_pitch = 4 + expected_roll = 5 + expected_yaw = 6 + actual_position = d.helices[0].position + expected_group_name = f'pitch_{expected_pitch}_yaw_{expected_yaw}' + expected_group = d.groups[expected_group_name] + actual_pitch = expected_group.pitch + actual_roll = d.helices[0].roll + actual_yaw = expected_group.yaw + self.assertEqual(expected_position, actual_position) + self.assertEqual(expected_pitch, actual_pitch) + self.assertEqual(expected_roll, actual_roll) + self.assertEqual(expected_yaw, actual_yaw) + self.assertEqual(expected_group_name, d.helices[0].group) + + def test_json_tristan_example_issue_32(self) -> None: + json_str = """ + { + "version": "0.3.0", + "grid": "square", + "helices": [ + {"grid_position": [0, 0]}, + {"max_offset": 32, "grid_position": [0, 1]} + ], + "strands": [ + { + "color": "#0066cc", + "domains": [ {"helix": 0, "forward": true, "start": 0, "end": 32} ], + "is_scaffold": true + } + ] + } + """ + sc.Design.from_scadnano_json_str(json_str) + + def test_to_json__hairpin(self) -> None: + """ + 01234 + AAACC # helix 0 going forward + \ + T # loopout + G # loopout + C # loopout + A # loopout + C # loopout + / + TTTGG # helix 0 going reverse + """ + ss_f = sc.Domain(helix=0, forward=True, start=0, end=5) + loop = sc.Loopout(length=5) + ss_r = sc.Domain(helix=0, forward=False, start=0, end=5) + strand_forward = sc.Strand([ss_f, loop, ss_r]) + design = sc.Design(strands=[strand_forward], grid=sc.square) + design.assign_dna(strand_forward, 'AAACC TGCAC') + design.to_json() + # should be no error getting here + + def test_to_json__roll(self) -> None: + helix = sc.Helix(roll=90) + ss_f = sc.Domain(helix=0, forward=True, start=0, end=5) + ss_r = sc.Domain(helix=0, forward=False, start=0, end=5) + strand_f = sc.Strand([ss_f]) + strand_r = sc.Strand([ss_r]) + design = sc.Design(helices=[helix], strands=[strand_f, strand_r], grid=sc.square) + design.to_json() + # should be no error getting here + + def test_from_json_extension_design(self) -> None: + json_str = """ + { + "version": "0.17.3", + "grid": "square", + "helices": [ + {"grid_position": [0, 0], "max_offset": 100} + ], + "strands": [ + { + "domains": [ + {"helix": 0, "forward": true, "start": 0, "end": 10}, + {"extension_num_bases": 5, "display_length": 1.4, "display_angle": 50.0} + ], + "is_scaffold": true + } + ] + } + """ + design = sc.Design.from_scadnano_json_str(json_str) + self.assertEqual( + sc.Extension(5, display_length=1.4, display_angle=50.0), + design.strands[0].domains[1]) + + def test_to_json_extension_design__extension(self) -> None: + # Setup + design = sc.Design(helices=[sc.Helix(max_offset=100)], strands=[], grid=sc.square) + design.draw_strand(0, 0).to(10).extension_3p(5) + + # Action + result = design.to_json() + + # Verify + document = json.loads(result) + self.assertEqual(2, len(document["strands"][0]["domains"])) + self.assertIn(sc.extension_key, document["strands"][0]["domains"][1]) + self.assertEqual(5, document["strands"][0]["domains"][1][sc.extension_key]) + + +class TestIllegalStructuresPrevented(unittest.TestCase): + + # def test_to_json__error_if_DNAOrigamiDesign_no_scaffold(self) -> None: + # # we are allowed to delay assigning a scaffold to a DNAOrigamiDesign, + # # but to_json should fail + # st_l = sc.Strand([ + # sc.Substrand(1, True, 0, 8, insertions=[(4, 2)]), + # sc.Substrand(0, False, 0, 8, deletions=[3]), + # ]) + # st_r = sc.Strand([ + # sc.Substrand(0, False, 8, 16), + # sc.Substrand(1, True, 8, 16), + # ]) + # scaf = sc.Strand([ + # sc.Substrand(1, False, 0, 8, insertions=[(4, 2)]), + # sc.Substrand(0, True, 0, 16, deletions=[3]), + # sc.Loopout(3), + # sc.Substrand(1, False, 8, 16, deletions=[]), + # ]) + # design_pre_json = sc.DNAOrigamiDesign(strands=[st_l, st_r, scaf], grid=sc.square) + # + # with self.assertRaises(sc.IllegalDesignError): + # design_pre_json.to_json() + + def test_domains_not_none_in_Strand_constructor(self) -> None: + with self.assertRaises(sc.IllegalDesignError): + sc.Strand(domains=None) # type: ignore + with self.assertRaises(sc.IllegalDesignError): + sc.Strand(domains=[None]) # type: ignore + + def test_strands_not_specified_in_Design_constructor(self) -> None: + design = sc.Design(helices=[]) + self.assertEqual(0, len(design.helices)) + self.assertEqual(0, len(design.strands)) + + def test_helices_not_specified_in_Design_constructor(self) -> None: + design = sc.Design(strands=[]) + self.assertEqual(0, len(design.helices)) + self.assertEqual(0, len(design.strands)) + + def test_strands_and_helices_not_specified_in_Design_constructor(self) -> None: + design = sc.Design() + self.assertEqual(0, len(design.helices)) + self.assertEqual(0, len(design.strands)) + + def test_consecutive_domains_loopout(self) -> None: + ss1 = sc.Domain(0, True, 0, 3) + ss2 = sc.Loopout(4) + ss3 = sc.Loopout(4) + with self.assertRaises(sc.IllegalDesignError): + sc.Strand([ss1, ss2, ss3]) + + # XXX: we used to allow Strands to violate the loopout rules and caught it only at the design level + # now the Strand constructor checks, so that means we can't set up a bad Strand for the Design check + # strand = sc.Strand([ss1, ss2]) + # strand.domains.append(ss3) + # helices = [sc.Helix(max_offset=10)] + # with self.assertRaises(sc.IllegalDesignError): + # sc.Design(helices=helices, strands=[strand], grid=sc.square) + + def test_singleton_loopout(self) -> None: + loopout = sc.Loopout(4) + with self.assertRaises(sc.StrandError): + sc.Strand([loopout]) + + # XXX: we used to allow Strands to violate the loopout rules and caught it only at the design level + # now the Strand constructor checks, so that means we can't set up a bad Strand for the Design check + # strand = sc.Strand([]) + # strand.domains.append(loopout) + # helices = [sc.Helix(max_offset=10)] + # with self.assertRaises(sc.StrandError): + # sc.Design(helices=helices, strands=[strand], grid=sc.square) + + def test_strand_offset_beyond_maxbases(self) -> None: + helices = [sc.Helix(max_offset=10)] + ss1 = sc.Domain(0, True, 0, 20) + strands = [sc.Strand([ss1])] + with self.assertRaises(sc.StrandError): + sc.Design(helices=helices, strands=strands) + + def test_assign_dna__conflicting_sequences_directly_assigned(self) -> None: + ss_right = sc.Domain(0, True, 0, 5) + ss_left = sc.Domain(0, False, 0, 5) + strand_right = sc.Strand([ss_right]) + strand_left = sc.Strand([ss_left]) + design = sc.Design(strands=[strand_left, strand_right]) + design.assign_dna(strand_right, 'ACGTT') + with self.assertRaises(sc.IllegalDesignError): + design.assign_dna(strand_right, 'TTTTT') + + def test_assign_dna__conflicting_sequences_indirectly_assigned(self) -> None: + ss_right = sc.Domain(0, True, 0, 5) + ss_left = sc.Domain(0, False, 0, 5) + strand_right = sc.Strand([ss_right]) + strand_left = sc.Strand([ss_left]) + design = sc.Design(strands=[strand_left, strand_right]) + design.assign_dna(strand_right, 'ACGTT') + with self.assertRaises(sc.IllegalDesignError): + design.assign_dna(strand_left, 'GGGGG') + + def test_overlapping_caught_in_strange_counterexample(self) -> None: + # found this counterexample as a simplified version of something caught in practice + s1_left_ss0 = sc.Domain(0, False, 0, 5) + s1_ss1 = sc.Domain(0, True, 0, 15) + s1_right_ss0 = sc.Domain(0, False, 5, 15) + s1 = sc.Strand([s1_left_ss0, s1_ss1, s1_right_ss0]) + + s2_ss1 = sc.Domain(0, True, 10, 20) + s2_ss0 = sc.Domain(0, False, 10, 20) + s2 = sc.Strand([s2_ss1, s2_ss0]) + + strands = [s1, s2] + + with self.assertRaises(sc.IllegalDesignError): + sc.Design(strands=strands, grid=sc.square) + + def test_major_tick_outside_range(self) -> None: + with self.assertRaises(sc.IllegalDesignError): + sc.Helix(max_offset=9, major_ticks=[2, 5, 10]) + + def test_major_tick_just_inside_range(self) -> None: + sc.Helix(max_offset=9, major_ticks=[0, 5, 9]) + + def test_two_illegally_overlapping_strands(self) -> None: + ss_bot = sc.Domain(helix=0, forward=False, start=0, end=9) + ss_top = sc.Domain(helix=0, forward=False, start=0, end=9) + strand_bot = sc.Strand(domains=[ss_bot]) + strand_top = sc.Strand(domains=[ss_top]) + strands = [strand_bot, strand_top] + with self.assertRaises(sc.IllegalDesignError): + sc.Design(grid=sc.square, strands=strands) + + def test_two_nonconsecutive_illegally_overlapping_strands(self) -> None: + ss_top1 = sc.Domain(helix=0, forward=False, start=0, end=5) + ss_bot = sc.Domain(helix=0, forward=True, start=2, end=9) + ss_top2 = sc.Domain(helix=0, forward=False, start=4, end=8) + strand_bot = sc.Strand(domains=[ss_bot]) + strand_top1 = sc.Strand(domains=[ss_top1]) + strand_top2 = sc.Strand(domains=[ss_top2]) + strands = [strand_bot, strand_top1, strand_top2] + with self.assertRaises(sc.IllegalDesignError): + sc.Design(grid=sc.square, strands=strands) + + def test_four_legally_leapfrogging_strands(self) -> None: + ss_top1 = sc.Domain(helix=0, forward=False, start=0, end=20) + ss_bot1 = sc.Domain(helix=0, forward=True, start=10, end=30) + ss_top2 = sc.Domain(helix=0, forward=False, start=20, end=40) + ss_bot2 = sc.Domain(helix=0, forward=True, start=30, end=50) + strand_bot1 = sc.Strand(domains=[ss_bot1]) + strand_bot2 = sc.Strand(domains=[ss_bot2]) + strand_top1 = sc.Strand(domains=[ss_top1]) + strand_top2 = sc.Strand(domains=[ss_top2]) + strands = [strand_bot1, strand_bot2, strand_top1, strand_top2] + sc.Design(grid=sc.square, strands=strands) + + def test_strand_references_nonexistent_helix(self) -> None: + h1 = sc.Helix(max_offset=9) + h2 = sc.Helix(max_offset=9) + ss_bot = sc.Domain(helix=2, forward=False, start=0, end=9) + ss_top = sc.Domain(helix=3, forward=False, start=0, end=9) + strand_bot = sc.Strand(domains=[ss_bot]) + strand_top = sc.Strand(domains=[ss_top]) + strands = [strand_bot, strand_top] + with self.assertRaises(sc.IllegalDesignError): + sc.Design(grid=sc.square, helices=[h1, h2], strands=strands) + + +class TestInsertRemoveDomains(unittest.TestCase): + + def setUp(self) -> None: + helices = [sc.Helix(max_offset=100) for _ in range(4)] + self.design = sc.Design(helices=helices, strands=[]) + self.design.draw_strand(0, 0).to(3).cross(1).to(0).cross(2).to(3).with_sequence('ACA TCT GTG') + self.strand = self.design.strands[0] + + def test_3_helix_before_design(self) -> None: + expected_strand_before = sc.Strand([ + sc.Domain(0, True, 0, 3), + sc.Domain(1, False, 0, 3), + sc.Domain(2, True, 0, 3), + ], dna_sequence='ACA TCT GTG'.replace(' ', '')) + self.assertEqual(expected_strand_before, self.strand) + + def test_insert_domain_with_sequence(self) -> None: + helices = [sc.Helix(max_offset=100) for _ in range(4)] + design = sc.Design(helices=helices, strands=[]) + design.draw_strand(0, 0).to(3).cross(1).to(0).cross(3).to(3).with_sequence('ACA TCT GTG') + strand = design.strands[0] + + expected_strand_before = sc.Strand([ + sc.Domain(0, True, 0, 3), + sc.Domain(1, False, 0, 3), + sc.Domain(3, True, 0, 3), + ], dna_sequence='ACA TCT GTG'.replace(' ', '')) + self.assertEqual(expected_strand_before, design.strands[0]) + + domain = sc.Domain(2, True, 0, 3) + design.insert_domain(strand, 2, domain) + expected_strand = sc.Strand([ + sc.Domain(0, True, 0, 3), + sc.Domain(1, False, 0, 3), + sc.Domain(2, True, 0, 3), + sc.Domain(3, True, 0, 3), + ], dna_sequence='ACA TCT ??? GTG'.replace(' ', '')) + self.assertEqual(expected_strand, design.strands[0]) + + def test_append_domain_with_sequence(self) -> None: + domain = sc.Domain(3, False, 0, 3) + self.design.append_domain(self.strand, domain) + expected_strand = sc.Strand([ + sc.Domain(0, True, 0, 3), + sc.Domain(1, False, 0, 3), + sc.Domain(2, True, 0, 3), + sc.Domain(3, False, 0, 3), + ], dna_sequence='ACA TCT GTG ???'.replace(' ', '')) + self.assertEqual(expected_strand, self.strand) + + def test_remove_first_domain_with_sequence(self) -> None: + self.design.remove_domain(self.strand, self.strand.domains[0]) + expected_strand = sc.Strand([ + sc.Domain(1, False, 0, 3), + sc.Domain(2, True, 0, 3), + ], dna_sequence=' TCT GTG'.replace(' ', '')) + self.assertEqual(expected_strand, self.strand) + + def test_remove_middle_domain_with_sequence(self) -> None: + self.design.remove_domain(self.strand, self.strand.domains[1]) + expected_strand = sc.Strand([ + sc.Domain(0, True, 0, 3), + sc.Domain(2, True, 0, 3), + ], dna_sequence='ACA GTG'.replace(' ', '')) + self.assertEqual(expected_strand, self.strand) + + def test_remove_last_domain_with_sequence(self) -> None: + self.design.remove_domain(self.strand, self.strand.domains[2]) + expected_strand = sc.Strand([ + sc.Domain(0, True, 0, 3), + sc.Domain(1, False, 0, 3), + ], dna_sequence='ACA TCT'.replace(' ', '')) + self.assertEqual(expected_strand, self.strand) + + +class TestLabels(unittest.TestCase): + + def setUp(self) -> None: + helices = [sc.Helix(max_offset=100) for _ in range(10)] + self.design = sc.Design(helices=helices, strands=[], grid=sc.square) + + def test_with_label__str(self) -> None: + label = 'abc' + self.design.draw_strand(0, 0).to(5).cross(1).to(0).with_label(label) + actual_strand = self.design.strands[0] + expected_strand = sc.Strand(domains=[ + sc.Domain(0, True, 0, 5), + sc.Domain(0, False, 0, 5), + ], label=label) + + self.assertEqual(expected_strand.label, actual_strand.label) + + def test_with_label__dict(self) -> None: + label = {'name': 'abc', 'type': 3} + self.design.draw_strand(0, 0).to(5).cross(1).to(0).with_label(label) + actual_strand = self.design.strands[0] + expected_strand = sc.Strand(domains=[ + sc.Domain(0, True, 0, 5), + sc.Domain(0, False, 0, 5), + ], label=label) + + self.assertDictEqual(expected_strand.label, actual_strand.label) + + def test_with_domain_label(self) -> None: + label0: Union[str, Dict[str, Any]] = 'abc' + label1: Union[str, Dict[str, Any]] = {'name': 'abc', 'type': 3} + self.design.draw_strand(0, 0).to(5).with_domain_label(label0).cross(1).to(0).with_domain_label(label1) + actual_strand = self.design.strands[0] + expected_strand = sc.Strand(domains=[ + sc.Domain(0, True, 0, 5, label=label0), + sc.Domain(0, False, 0, 5, label=label1), + ]) + + self.assertEqual(expected_strand.domains[0].label, actual_strand.domains[0].label) + self.assertDictEqual(expected_strand.domains[1].label, actual_strand.domains[1].label) + + def test_with_domain_label__and__with_label(self) -> None: + strand_label = 'xyz' + label0: Union[str, Dict[str, Any]] = 'abc' + label1: Union[str, Dict[str, Any]] = {'name': 'abc', 'type': 3} + self.design.draw_strand(0, 0).to(5).with_domain_label(label0).cross(1).to(0).with_domain_label(label1) \ + .with_label(strand_label) + actual_strand = self.design.strands[0] + expected_strand = sc.Strand(domains=[ + sc.Domain(0, True, 0, 5, label=label0), + sc.Domain(0, False, 0, 5, label=label1), + ], label=strand_label) + + self.assertEqual(expected_strand.label, actual_strand.label) + self.assertEqual(expected_strand.domains[0].label, actual_strand.domains[0].label) + self.assertDictEqual(expected_strand.domains[1].label, actual_strand.domains[1].label) + + +def set_colors_black(*strands) -> None: + for strand in strands: + strand.set_color(sc.Color(r=0, g=0, b=0)) + + +class TestCircularStrandsLegalMods(unittest.TestCase): + ''' + Tests that circular strands cannot have 5' or 3' mods. + ''' + + def setUp(self) -> None: + helices = [sc.Helix(max_offset=10) for _ in range(2)] + self.design = sc.Design(helices=helices, strands=[]) + self.design.draw_strand(0, 0).move(10).cross(1).move(-10) + self.strand = self.design.strands[0] + r''' + 0 [--------\ + | + 1 <--------/ + ''' + + def test_can_add_internal_mod_to_circular_strand(self) -> None: + self.strand.set_circular() + self.assertTrue(self.strand.circular) + self.strand.set_modification_internal(2, mod.biotin_int) + self.assertEqual(1, len(self.strand.modifications_int)) + + def test_cannot_make_strand_circular_if_5p_mod(self) -> None: + self.strand.set_modification_5p(mod.biotin_5p) + with self.assertRaises(sc.StrandError): + self.strand.set_circular(True) + + def test_cannot_make_strand_circular_if_3p_mod(self) -> None: + self.strand.set_modification_3p(mod.biotin_3p) + with self.assertRaises(sc.StrandError): + self.strand.set_circular(True) + + def test_add_5p_mod_to_circular_strand(self) -> None: + self.strand.set_circular(True) + with self.assertRaises(sc.StrandError): + self.strand.set_modification_5p(mod.biotin_5p) + + def test_add_3p_mod_to_circular_strand(self) -> None: + self.strand.set_circular(True) + with self.assertRaises(sc.StrandError): + self.strand.set_modification_3p(mod.biotin_3p) + + +class TestCircularStrandEdits(unittest.TestCase): + ''' + Tests that circular strand edits (nicking, ligating, adding crossovers/loopouts) + works properly with circular strands. + ''' + + def setUp(self) -> None: + helices = [sc.Helix(max_offset=50) for _ in range(3)] + self.design = sc.Design(helices=helices, strands=[]) + self.design.draw_strand(0, 0).move(10).cross(1).move(-10) + self.design.draw_strand(0, 15).move(5).cross(1).move(-10).cross(0).move(5) + self.design.draw_strand(0, 20).move(10) + self.design.draw_strand(1, 30).move(-10) + self.design.draw_strand(0, 30).move(10).loopout(1, 5).move(-10).cross(2).move(10).as_circular() + self.design.draw_strand(0, 40).move(10).cross(1).move(-10).as_circular() + self.num_strands = len(self.design.strands) + r''' + 0 10 20 30 40 + strand 0 strand 1 strand 2 strand 4 strand 5 + 0 [--------\ /--->[---\ [--------> [--------\ /--------\ + | | | ) | | + 1 <--------/ \--------/ <--------] /--------/ \--------/ + strand 3 | + 2 \-------->cross to 5' end here + ''' + + def test_add_crossover_from_linear_strand_to_itself_makes_it_circular(self) -> None: + # add crossover to strand 0 + r''' + 0 + strand 0 + 0 /--------\ + | | + 1 \--------/ + ''' + self.assertEqual(2, len(self.design.strands[0].domains)) + self.assertFalse(self.design.strands[0].circular) + self.design.add_half_crossover(0, 1, 0, True) + self.assertTrue(self.design.strands[0].circular) + self.assertEqual(self.num_strands, len(self.design.strands)) + self.assertEqual(2, len(self.design.strands[0].domains)) + + def test_add_nick_to_2_domain_circular_strand_makes_it_linear_nick_first_domain(self) -> None: + r''' + 40 + strand 5 + 0 /--->[---\ + | | + 1 \--------/ + ''' + self.assertTrue(self.design.strands[5].circular) + + # nick strand 5 + self.design.add_nick(0, 45, True) + self.assertFalse(self.design.strands[5].circular) + self.assertEqual(self.num_strands, len(self.design.strands)) + + strand = self.design.strands[5] + self.assertEqual(3, len(strand.domains)) + self.assertEqual(3, len(strand.bound_domains())) + + d0, d1, d2 = strand.bound_domains() + self.assertEqual(0, d0.helix) + self.assertEqual(True, d0.forward) + self.assertEqual(45, d0.start) + self.assertEqual(50, d0.end) + + self.assertEqual(1, d1.helix) + self.assertEqual(False, d1.forward) + self.assertEqual(40, d1.start) + self.assertEqual(50, d1.end) + + self.assertEqual(0, d2.helix) + self.assertEqual(True, d2.forward) + self.assertEqual(40, d2.start) + self.assertEqual(45, d2.end) + + for domain in strand.domains: + self.assertIs(strand, domain.strand()) + + def test_add_nick_to_2_domain_circular_strand_makes_it_linear_nick_second_domain(self) -> None: + # nick strand 5 + r''' + 40 + strand 5 + 0 /--------\ + | | + 1 \---]<---/ + ''' + self.design.add_nick(1, 45, False) + strand = self.design.strands[5] + self.assertFalse(strand.circular) + self.assertEqual(self.num_strands, len(self.design.strands)) + + self.assertEqual(3, len(strand.domains)) + self.assertEqual(3, len(strand.bound_domains())) + + d0, d1, d2 = strand.bound_domains() + self.assertEqual(1, d0.helix) + self.assertEqual(False, d0.forward) + self.assertEqual(40, d0.start) + self.assertEqual(45, d0.end) + + self.assertEqual(0, d1.helix) + self.assertEqual(True, d1.forward) + self.assertEqual(40, d1.start) + self.assertEqual(50, d1.end) + + self.assertEqual(1, d2.helix) + self.assertEqual(False, d2.forward) + self.assertEqual(45, d2.start) + self.assertEqual(50, d2.end) + + for domain in strand.domains: + self.assertIs(strand, domain.strand()) + + def test_add_nick_to_3_domain_circular_strand_makes_it_linear_nick_first_domain(self) -> None: + # nick strand 4 + r''' + 30 40 + strand 4 + 0 [--->[---\ + ) loopout length 5 + 1 /--------/ + | + 2 \-------->cross to 5' end here + ''' + self.design.add_nick(0, 35, True) + strand = self.design.strands[4] + self.assertFalse(strand.circular) + self.assertEqual(self.num_strands, len(self.design.strands)) + + self.assertEqual(5, len(strand.domains)) + self.assertEqual(4, len(strand.bound_domains())) + + d0, loopout, d1, d2, d3 = strand.domains + self.assertIsInstance(loopout, sc.Loopout) + + self.assertEqual(0, d0.helix) + self.assertEqual(True, d0.forward) + self.assertEqual(35, d0.start) + self.assertEqual(40, d0.end) + + self.assertEqual(1, d1.helix) + self.assertEqual(False, d1.forward) + self.assertEqual(30, d1.start) + self.assertEqual(40, d1.end) + + self.assertEqual(2, d2.helix) + self.assertEqual(True, d2.forward) + self.assertEqual(30, d2.start) + self.assertEqual(40, d2.end) + + self.assertEqual(0, d3.helix) + self.assertEqual(True, d3.forward) + self.assertEqual(30, d3.start) + self.assertEqual(35, d3.end) + + for domain in strand.domains: + self.assertIs(strand, domain.strand()) + + def test_add_nick_to_3_domain_circular_strand_makes_it_linear_nick_middle_domain(self) -> None: + # nick strand 4 + r''' + 30 40 + strand 4 + 0 [--------\ + ) loopout length 5 + 1 /---]<---/ + | + 2 \-------->cross to 5' end here + ''' + self.design.add_nick(1, 35, False) + strand = self.design.strands[4] + self.assertFalse(strand.circular) + self.assertEqual(self.num_strands, len(self.design.strands)) + + self.assertEqual(5, len(strand.domains)) + self.assertEqual(4, len(strand.bound_domains())) + + d0, d1, d2, loopout, d3 = strand.domains + self.assertIsInstance(loopout, sc.Loopout) + + self.assertEqual(1, d0.helix) + self.assertEqual(False, d0.forward) + self.assertEqual(30, d0.start) + self.assertEqual(35, d0.end) + + self.assertEqual(2, d1.helix) + self.assertEqual(True, d1.forward) + self.assertEqual(30, d1.start) + self.assertEqual(40, d1.end) + + self.assertEqual(0, d2.helix) + self.assertEqual(True, d2.forward) + self.assertEqual(30, d2.start) + self.assertEqual(40, d2.end) + + self.assertEqual(1, d3.helix) + self.assertEqual(False, d3.forward) + self.assertEqual(35, d3.start) + self.assertEqual(40, d3.end) + + for domain in strand.domains: + self.assertIs(strand, domain.strand()) + + def test_add_nick_to_3_domain_circular_strand_makes_it_linear_nick_last_domain(self) -> None: + # nick strand 4 + r''' + 30 40 + strand 4 + 0 [--------\ + ) loopout length 5 + 1 /--------/ + | + 2 \--->[--->cross to 5' end here + ''' + self.design.add_nick(2, 35, True) + strand = self.design.strands[4] + self.assertFalse(strand.circular) + self.assertEqual(self.num_strands, len(self.design.strands)) + + self.assertEqual(5, len(strand.domains)) + self.assertEqual(4, len(strand.bound_domains())) + + d0, d1, loopout, d2, d3 = strand.domains + self.assertIsInstance(loopout, sc.Loopout) + + self.assertEqual(2, d0.helix) + self.assertEqual(True, d0.forward) + self.assertEqual(35, d0.start) + self.assertEqual(40, d0.end) + + self.assertEqual(0, d1.helix) + self.assertEqual(True, d1.forward) + self.assertEqual(30, d1.start) + self.assertEqual(40, d1.end) + + self.assertEqual(1, d2.helix) + self.assertEqual(False, d2.forward) + self.assertEqual(30, d2.start) + self.assertEqual(40, d2.end) + + self.assertEqual(2, d3.helix) + self.assertEqual(True, d3.forward) + self.assertEqual(30, d3.start) + self.assertEqual(35, d3.end) + + for domain in strand.domains: + self.assertIs(strand, domain.strand()) + + def test_ligate_linear_strand_to_itself_makes_it_circular(self) -> None: + self.assertFalse(self.design.strands[1].circular) + self.assertEqual(3, len(self.design.strands[1].domains)) + + self.design.ligate(0, 15, True) + + strand = self.design.strands[1] + self.assertEqual(self.num_strands, len(self.design.strands)) + self.assertTrue(strand.circular) + self.assertEqual(2, len(strand.domains)) + + for domain in strand.domains: + self.assertIs(strand, domain.strand()) + + # TODO: add functionality for removing crossovers and loopouts, and test that here + + +class TestAddStrand(unittest.TestCase): + + def test_add_strand__with_loopout(self) -> None: + helices = [sc.Helix(max_offset=10), sc.Helix(max_offset=10)] + design = sc.Design(helices=helices, strands=[]) + + ss1 = sc.Domain(0, True, 0, 10) + loop = sc.Loopout(4) + ss2 = sc.Domain(1, False, 0, 10) + strand = sc.Strand([ss1, loop, ss2]) + + design.add_strand(strand) + + self.assertEqual(1, len(design.strands)) + self.assertEqual(strand, design.strands[0]) + self.assertEqual(ss1, design.domain_at(0, 0, True)) + self.assertEqual(ss2, design.domain_at(1, 0, False)) + + def test_add_strand__illegal_overlapping_domains(self) -> None: + helices = [sc.Helix(max_offset=50), sc.Helix(max_offset=50)] + design = sc.Design(helices=helices, strands=[], grid=sc.square) + with self.assertRaises(sc.StrandError): + strand = sc.Strand([ + sc.Domain(0, False, 40, 48), + sc.Domain(0, False, 32, 48, deletions=[44]), + sc.Domain(1, True, 32, 40), + ]) + design.add_strand(strand) + + +class TestAssignDNA(unittest.TestCase): + + def test_assign_dna__hairpin(self) -> None: + """ + 01234 + AAACC # helix 0 going forward + \ + T # loopout + G # loopout + C # loopout + A # loopout + C # loopout + / + TTTGG # helix 0 going reverse + """ + ss_f = sc.Domain(helix=0, forward=True, start=0, end=5) + loop = sc.Loopout(length=5) + ss_r = sc.Domain(helix=0, forward=False, start=0, end=5) + strand_forward = sc.Strand([ss_f, loop, ss_r]) + design = sc.Design(strands=[strand_forward], grid=sc.square) + design.assign_dna(strand_forward, 'AAACC TGCAC') + self.assertEqual('AAACC TGCAC GGTTT'.replace(' ', ''), strand_forward.dna_sequence) + + def test_assign_dna__from_strand_with_loopout(self) -> None: + """ + 01234 + <-TTTGG-] + [-AAACC- # helix 0 + \ + T # loopout + G # loopout + C # loopout + A # loopout + C # loopout + / + <-GCTTA- # helix 1 + [-CGAAT-> + """ + ss_f = sc.Domain(helix=0, forward=True, start=0, end=5) + loop = sc.Loopout(length=5) + ss_r = sc.Domain(helix=1, forward=False, start=0, end=5) + strand_multi = sc.Strand([ss_f, loop, ss_r]) + + ss_single0 = sc.Domain(helix=0, forward=False, start=0, end=5) + strand_single0 = sc.Strand([ss_single0]) + + ss_single1 = sc.Domain(helix=1, forward=True, start=0, end=5) + strand_single1 = sc.Strand([ss_single1]) + + design = sc.Design(strands=[strand_multi, strand_single0, strand_single1], grid=sc.square) + + design.assign_dna(strand_multi, 'AAACC TGCAC ATTCG') + + self.assertEqual('AAACC TGCAC ATTCG'.replace(' ', ''), strand_multi.dna_sequence) + self.assertEqual('GGTTT'.replace(' ', ''), strand_single0.dna_sequence) + self.assertEqual('CGAAT'.replace(' ', ''), strand_single1.dna_sequence) + + def test_assign_dna__to_strand_with_loopout(self) -> None: + """ + 01234 + <-TTTGG-] + [-AAACC- # helix 0 + \ + ? # loopout + ? # loopout + ? # loopout + ? # loopout + ? # loopout + / + <-GCTTA- # helix 1 + [-CGAAT-> + """ + ss_f = sc.Domain(helix=0, forward=True, start=0, end=5) + loop = sc.Loopout(length=5) + ss_r = sc.Domain(helix=1, forward=False, start=0, end=5) + strand_multi = sc.Strand([ss_f, loop, ss_r]) + + ss_single0 = sc.Domain(helix=0, forward=False, start=0, end=5) + strand_single0 = sc.Strand([ss_single0]) + + ss_single1 = sc.Domain(helix=1, forward=True, start=0, end=5) + strand_single1 = sc.Strand([ss_single1]) + + design = sc.Design(strands=[strand_multi, strand_single0, strand_single1], grid=sc.square) + + design.assign_dna(strand_single0, 'GGTTT') + + self.assertEqual('AAACC ????? ?????'.replace(' ', ''), strand_multi.dna_sequence) + self.assertEqual('GGTTT'.replace(' ', ''), strand_single0.dna_sequence) + + design.assign_dna(strand_single1, 'CGAAT') + + self.assertEqual('AAACC ????? ATTCG'.replace(' ', ''), strand_multi.dna_sequence) + self.assertEqual('GGTTT'.replace(' ', ''), strand_single0.dna_sequence) + self.assertEqual('CGAAT'.replace(' ', ''), strand_single1.dna_sequence) + + def test_assign_dna__assign_from_strand_multi_other_single(self) -> None: + """ + 01234567 + <-TTTG----GACA-] + +-AAAC->[-CTGT-+ # helix 0 + | | + +-GCTT----AGTA-+ # helix 1 + """ + ss_f_left = sc.Domain(helix=0, forward=True, start=0, end=4) + ss_f_right = sc.Domain(helix=0, forward=True, start=4, end=8) + ss_h1 = sc.Domain(helix=1, forward=False, start=0, end=8) + strand_multi = sc.Strand([ss_f_right, ss_h1, ss_f_left]) + + ss_single = sc.Domain(helix=0, forward=False, start=0, end=8) + strand_single = sc.Strand([ss_single]) + + design = sc.Design(strands=[strand_multi, strand_single], grid=sc.square) + + design.assign_dna(strand_multi, 'CTGT ATGA TTCG AAAC') + + self.assertEqual('CTGT ATGA TTCG AAAC'.replace(' ', ''), strand_multi.dna_sequence) + self.assertEqual('ACAG GTTT'.replace(' ', ''), strand_single.dna_sequence) + + def test_assign_dna__assign_to_strand_multi_other_single(self) -> None: + """ + 01234567 + <-TTTG----GACA-] + +-AAAC->[-CTGT-+ # helix 0 + | | + +-????----????-+ # helix 1 + """ + ss_f_left = sc.Domain(helix=0, forward=True, start=0, end=4) + ss_f_right = sc.Domain(helix=0, forward=True, start=4, end=8) + ss_h1 = sc.Domain(helix=1, forward=False, start=0, end=8) + strand_multi = sc.Strand([ss_f_right, ss_h1, ss_f_left]) + + ss_single = sc.Domain(helix=0, forward=False, start=0, end=8) + strand_single = sc.Strand([ss_single]) + + design = sc.Design(strands=[strand_multi, strand_single], grid=sc.square) + + design.assign_dna(strand_single, 'ACAG GTTT') + + self.assertEqual('CTGT ???? ???? AAAC'.replace(' ', ''), strand_multi.dna_sequence) + self.assertEqual('ACAG GTTT'.replace(' ', ''), strand_single.dna_sequence) + + def test_assign_dna__other_strand_fully_defined_already(self) -> None: + """ + 01234567 + [------> + CAAAGTCG + GTTT + <--] + """ + ss_r = sc.Domain(helix=0, forward=True, start=0, end=8) + ss_l = sc.Domain(helix=0, forward=False, start=0, end=4) + strand_r = sc.Strand(domains=[ss_r]) + strand_l = sc.Strand(domains=[ss_l]) + design = sc.Design(grid=sc.square, strands=[strand_r, strand_l]) + design.assign_dna(strand_r, 'CAAAGTCG') + design.assign_dna(strand_l, 'TTTG') + # should not have an error by this point + + def test_assign_dna__other_strand_fully_defined_already_and_other_extends_beyond(self) -> None: + """ + 01234567 + [------> + CAAAGTCG + TTCA + <--] + """ + ss_r = sc.Domain(helix=0, forward=True, start=0, end=8) + ss_l = sc.Domain(helix=0, forward=False, start=2, end=6) + strand_r = sc.Strand(domains=[ss_r]) + strand_l = sc.Strand(domains=[ss_l]) + design = sc.Design(grid=sc.square, strands=[strand_r, strand_l]) + design.assign_dna(strand_r, 'CAAAGTCG') + design.assign_dna(strand_l, 'ACTT') + # should not have an error by this point + + def test_assign_dna__other_strand_fully_defined_already_and_self_extends_beyond(self) -> None: + """ + 01234567 + [------> + CAAAGTCG + TTCA + <--] + """ + ss_r = sc.Domain(helix=0, forward=True, start=0, end=8) + ss_l = sc.Domain(helix=0, forward=False, start=2, end=6) + strand_r = sc.Strand(domains=[ss_r]) + strand_l = sc.Strand(domains=[ss_l]) + design = sc.Design(grid=sc.square, strands=[strand_r, strand_l]) + design.assign_dna(strand_l, 'ACTT') + design.assign_dna(strand_r, 'CAAAGTCG') + # should not have an error by this point + + def test_assign_dna__two_equal_length_strands_on_one_helix(self) -> None: + """ + 01234 + <---] + CAAAA + GTTTT + [---> + """ + ss_r = sc.Domain(helix=0, forward=True, start=0, end=5) + ss_l = sc.Domain(helix=0, forward=False, start=0, end=5) + strand_r = sc.Strand(domains=[ss_r]) + strand_l = sc.Strand(domains=[ss_l]) + design = sc.Design(grid=sc.square, strands=[strand_r, strand_l]) + design.assign_dna(strand_l, 'AAAAC') + self.assertEqual('GTTTT', strand_r.dna_sequence) + + def test_assign_dna__assign_seq_with_wildcards(self) -> None: + """ + 01234 + <---] + C??AA + G??TT + [---> + """ + ss_bot = sc.Domain(helix=0, forward=True, start=0, end=5) + ss_top = sc.Domain(helix=0, forward=False, start=0, end=5) + strand_bot = sc.Strand(domains=[ss_bot]) + strand_top = sc.Strand(domains=[ss_top]) + strands = [strand_bot, strand_top] + design = sc.Design(grid=sc.square, strands=strands) + design.assign_dna(strand_top, 'AA??C') + self.assertEqual('G??TT', strand_bot.dna_sequence) + + def test_assign_dna__one_strand_assigned_by_complement_from_two_other_strands(self) -> None: + """ + 0123 4567 + <-AAAC-] <-GGGA-] + [-TTTG-----CCCT-> + """ + ss_top_left = sc.Domain(0, False, 0, 4) + ss_top_right = sc.Domain(0, False, 4, 8) + ss_bot = sc.Domain(0, True, 0, 8) + st_top_left = sc.Strand([ss_top_left]) + st_top_right = sc.Strand([ss_top_right]) + st_bot = sc.Strand([ss_bot]) + design = sc.Design(strands=[st_bot, st_top_left, st_top_right], grid=sc.square) + design.assign_dna(st_top_left, 'CAAA') + self.assertEqual('TTTG????', st_bot.dna_sequence) + design.assign_dna(st_top_right, 'AGGG') + self.assertEqual('TTTGCCCT', st_bot.dna_sequence) + + def test_assign_dna__adapter_assigned_from_scaffold_and_tiles(self) -> None: + """ + XXX: it appears the behavior this tests (which the other tests miss) is assigning DNA to + tile0 first, then to tile1, and adap is connected to each of them on different helices. + This means that when tile1 is assigned, we need to ensure when assigning to adap that we + keep the old information and don't discard it by simply padding the shorter portion of it on + helix 1 with ?'s, but remember the old DNA sequence. + + 01 2345 6789 01 + adap [-TTTC-----CATT-------+ + scaf <-GT-AAAG-+ <-GTAA--AA-] | + | | + [-AA-TTTG-+ [-TGCC--GG-> | + <-AAAC-----ACGG-------+ + """ + scaf0_ss = sc.Domain(0, False, 0, 6) + scaf1_ss = sc.Domain(1, True, 0, 6) + tile1_ss = sc.Domain(1, True, 6, 12) + tile0_ss = sc.Domain(0, False, 6, 12) + adap0_ss = sc.Domain(0, True, 2, 10) + adap1_ss = sc.Domain(1, False, 2, 10) + scaf = sc.Strand([scaf1_ss, scaf0_ss]) + adap = sc.Strand([adap0_ss, adap1_ss]) + tile0 = sc.Strand([tile0_ss]) + tile1 = sc.Strand([tile1_ss]) + + design = sc.Design(strands=[scaf, adap, tile0, tile1]) + + design.assign_dna(tile0, 'AA AATG') + self.assertEqual('???? CATT ???? ????'.replace(' ', ''), adap.dna_sequence) + + design.assign_dna(tile1, 'TGCC GG') + self.assertEqual('???? CATT GGCA ????'.replace(' ', ''), adap.dna_sequence) + + design.assign_dna(scaf, 'AA TTTG GAAA TG') + self.assertEqual('TTTC CATT GGCA CAAA'.replace(' ', ''), adap.dna_sequence) + + def test_assign_dna__adapter_assigned_from_scaffold_and_tiles_with_deletions(self) -> None: + """ + XXX: it appears the behavior this tests (which the other tests miss) is assigning DNA to + tile0 first, then to tile1, and adap is connected to each of them on different helices. + This means that when tile1 is assigned, we need to ensure when assigning to adap that we + keep the old information and don't discard it by simply padding the shorter portion of it on + helix 1 with ?'s, but remember the old DNA sequence. + + 01 2345 6789 01 + X X deletions + adap [-T TC-----CA T-------+ + scaf <-GT-A AG-+ <-GT A--AA-] | + | | + [-AA-TTTG-+ [-TG C--GG-> | + <-AAAC-----AC G-------+ + X deletions + """ + scaf0_ss = sc.Domain(0, False, 0, 6) + scaf1_ss = sc.Domain(1, True, 0, 6) + tile1_ss = sc.Domain(1, True, 6, 12) + tile0_ss = sc.Domain(0, False, 6, 12) + adap0_ss = sc.Domain(0, True, 2, 10) + adap1_ss = sc.Domain(1, False, 2, 10) + scaf = sc.Strand([scaf1_ss, scaf0_ss]) + adap = sc.Strand([adap0_ss, adap1_ss]) + tile0 = sc.Strand([tile0_ss]) + tile1 = sc.Strand([tile1_ss]) + + design = sc.Design(strands=[scaf, adap, tile0, tile1]) + design.add_deletion(0, 3) + design.add_deletion(0, 8) + design.add_deletion(1, 8) + + design.assign_dna(tile0, 'AA ATG') + self.assertEqual('??? CAT ??? ????'.replace(' ', ''), adap.dna_sequence) + + design.assign_dna(tile1, 'TGC GG') + self.assertEqual('??? CAT GCA ????'.replace(' ', ''), adap.dna_sequence) + + design.assign_dna(scaf, 'AA TTTG GAA TG') + self.assertEqual('TTC CAT GCA CAAA'.replace(' ', ''), adap.dna_sequence) + + def test_assign_dna__adapter_assigned_from_scaffold_and_tiles_with_insertions(self) -> None: + """ + XXX: it appears the behavior this tests (which the other tests miss) is assigning DNA to + tile0 first, then to tile1, and adap is connected to each of them on different helices. + This means that when tile1 is assigned, we need to ensure when assigning to adap that we + keep the old information and don't discard it by simply padding the shorter portion of it on + helix 1 with ?'s, but remember the old DNA sequence. + + 01 2345 678I9 01 + I insertions + adap [-TTTC-----CATTT-------+ + scaf <-GT-AAAG-+ <-GTAAA--AA-] | + | | + [-AA-TTTG-+ [-TGCCC--GG-> | + <-AAAC-----ACGGG-------+ + I insertions + """ + scaf0_ss = sc.Domain(0, False, 0, 6) + scaf1_ss = sc.Domain(1, True, 0, 6) + tile1_ss = sc.Domain(1, True, 6, 12) + tile0_ss = sc.Domain(0, False, 6, 12) + adap0_ss = sc.Domain(0, True, 2, 10) + adap1_ss = sc.Domain(1, False, 2, 10) + scaf = sc.Strand([scaf1_ss, scaf0_ss]) + adap = sc.Strand([adap0_ss, adap1_ss]) + tile0 = sc.Strand([tile0_ss]) + tile1 = sc.Strand([tile1_ss]) + + design = sc.Design(strands=[scaf, adap, tile0, tile1]) + design.add_insertion(0, 8, 1) + design.add_insertion(1, 8, 1) + + design.assign_dna(tile0, 'AA AAATG') + self.assertEqual('???? CATTT ????? ????'.replace(' ', ''), adap.dna_sequence) + + design.assign_dna(tile1, 'TGCCC GG') + self.assertEqual('???? CATTT GGGCA ????'.replace(' ', ''), adap.dna_sequence) + + design.assign_dna(scaf, 'AA TTTG GAAA TG') + self.assertEqual('TTTC CATTT GGGCA CAAA'.replace(' ', ''), adap.dna_sequence) + + def test_assign_dna__dna_sequence_shorter_than_complementary_strand_right_strand_longer(self) -> None: + """ + <---] + CAAAA + GTTTT????? + [--------> + """ + ss_long = sc.Domain(helix=0, forward=True, start=0, end=10) + ss_short = sc.Domain(helix=0, forward=False, start=0, end=5) + strand_long = sc.Strand(domains=[ss_long]) + strand_short = sc.Strand(domains=[ss_short]) + strands = [strand_long, strand_short] + design = sc.Design(grid=sc.square, strands=strands) + design.assign_dna(strand_short, 'AAAAC') + self.assertEqual('GTTTT?????', strand_long.dna_sequence) + + def test_assign_dna__dna_sequence_shorter_than_complementary_strand_left_strand_longer(self) -> None: + """ + [---> + AAAAC + TTTTG????? + <--------] + """ + ss_long = sc.Domain(helix=0, forward=False, start=0, end=10) + ss_short = sc.Domain(helix=0, forward=True, start=0, end=5) + strand_long = sc.Strand(domains=[ss_long]) + strand_short = sc.Strand(domains=[ss_short]) + strands = [strand_long, strand_short] + design = sc.Design(grid=sc.square, strands=strands) + design.assign_dna(strand_short, 'AAAAC') + self.assertEqual('?????GTTTT', strand_long.dna_sequence) + + def test_assign_dna__dna_sequence_with_uncomplemented_domain_on_different_helix(self) -> None: + """ + <---] + CAAAA + GTTTT????? + [--------+ + | + <-+ + ??? + """ + ss_long = sc.Domain(helix=0, forward=True, start=0, end=10) + ss_long_h1 = sc.Domain(helix=0, forward=False, start=7, end=10) + ss_short = sc.Domain(helix=0, forward=False, start=0, end=5) + strand_long = sc.Strand(domains=[ss_long, ss_long_h1]) + strand_short = sc.Strand(domains=[ss_short]) + strands = [strand_long, strand_short] + design = sc.Design(grid=sc.square, strands=strands) + design.assign_dna(strand_short, 'AAAAC') + self.assertEqual('GTTTT????????', strand_long.dna_sequence) + + def test_assign_dna__dna_sequence_with_uncomplemented_domain_on_different_helix_wildcards_both_ends( + self) -> None: + """ + <---] + CAAAA + ?????GTTTT + [--------+ + | + <-+ + ??? + """ + ss_long_h0 = sc.Domain(helix=0, forward=True, start=0, end=10) + ss_long_h1 = sc.Domain(helix=1, forward=False, start=7, end=10) + ss_short_h0 = sc.Domain(helix=0, forward=False, start=5, end=10) + strand_long = sc.Strand(domains=[ss_long_h0, ss_long_h1]) + strand_short = sc.Strand(domains=[ss_short_h0]) + strands = [strand_long, strand_short] + design = sc.Design(grid=sc.square, strands=strands) + design.assign_dna(strand_short, 'AAAAC') + self.assertEqual('?????GTTTT???', strand_long.dna_sequence) + + def test_assign_dna__one_helix_with_one_bottom_strand_and_three_top_strands(self) -> None: + """ + 012 345 678 + -TTT> -GGG> -CCC> + None: + """ + 012 345 678 901 + M13 [-ACC----TAA---GAA----AAC---+ + +-TGG-]<-ATT-+ CTT----TTG-+ | + | | | | | + +-GAT----TTC-+ ATG->[-AGT-+ | + <-CTA----AAG---TAC----TCA---+ + """ + scaf0_ss = sc.Domain(helix=0, forward=True, start=0, end=12) + scaf1_ss = sc.Domain(helix=1, forward=False, start=0, end=12) + scaf = sc.Strand(domains=[scaf0_ss, scaf1_ss]) + + first_stap0_left_ss = sc.Domain(helix=0, forward=False, start=0, end=3) + first_stap1_ss = sc.Domain(helix=1, forward=True, start=0, end=6) + first_stap0_right_ss = sc.Domain(helix=0, forward=False, start=3, end=6) + first_stap = sc.Strand(domains=[first_stap0_left_ss, first_stap1_ss, first_stap0_right_ss]) + + second_stap1_right_ss = sc.Domain(helix=1, forward=True, start=9, end=12) + second_stap0_ss = sc.Domain(helix=0, forward=False, start=6, end=12) + second_stap1_left_ss = sc.Domain(helix=1, forward=True, start=6, end=9) + second_stap = sc.Strand(domains=[second_stap1_right_ss, second_stap0_ss, second_stap1_left_ss]) + + strands = [scaf, first_stap, second_stap] + design = sc.Design(grid=sc.square, strands=strands) + design.assign_dna(scaf, 'ACC TAA GAA AAC ACT CAT GAA ATC'.replace(' ', '')) + self.assertEqual('GGT GAT TTC TTA'.replace(' ', ''), first_stap.dna_sequence) + self.assertEqual('AGT GTT TTC ATG'.replace(' ', ''), second_stap.dna_sequence) + + def test_assign_dna__upper_left_edge_staple_of_16H_origami_rectangle(self) -> None: + """ + staple | + | | + +-GATTTTGTGAGTAGAA- | + -CTAAAACACTCATCTT--+ + """ + scaf0_ss = sc.Domain(helix=0, forward=True, start=0, end=16) + scaf1_ss = sc.Domain(helix=1, forward=False, start=0, end=16) + stap0_ss = sc.Domain(helix=0, forward=False, start=0, end=16) + stap1_ss = sc.Domain(helix=1, forward=True, start=0, end=16) + scaf = sc.Strand(domains=[scaf1_ss, scaf0_ss]) + stap = sc.Strand(domains=[stap1_ss, stap0_ss]) + strands = [scaf, stap] + design = sc.Design(grid=sc.square, strands=strands) + + seq_m13_upper_left = 'AAGATGAGTGTTTTAGTGTATTCTTTTGCCTC' + design.assign_dna(scaf, seq_m13_upper_left) + expected_seq_stap_upperleft = 'CTAAAACACTCATCTTGAGGCAAAAGAATACA' + self.assertEqual(expected_seq_stap_upperleft, stap.dna_sequence) + + def test_assign_dna__2helix_with_deletions(self) -> None: + r""" + scaf index: 2 3 4 5 + offset: 0 D1 2 3 D4 5 + + - - + + /C A T C\ + | G T A G | + helix 0 | < + + ] | + | | | | + helix 1 | [ + + > | + | T T A C | + \A A T G/ + + ] < + + offset: 0 D1 2 3 D4 5 + scaf index: 1 0 7 6 + """ + width = 6 + width_h = width // 2 + helices = [sc.Helix(max_offset=width), sc.Helix(max_offset=width)] + stap_left_ss1 = sc.Domain(1, True, 0, width_h) + stap_left_ss0 = sc.Domain(0, False, 0, width_h) + stap_right_ss0 = sc.Domain(0, False, width_h, width) + stap_right_ss1 = sc.Domain(1, True, width_h, width) + scaf_ss1_left = sc.Domain(1, False, 0, width_h) + scaf_ss0 = sc.Domain(0, True, 0, width) + scaf_ss1_right = sc.Domain(1, False, width_h, width) + stap_left = sc.Strand([stap_left_ss1, stap_left_ss0]) + stap_right = sc.Strand([stap_right_ss0, stap_right_ss1]) + scaf = sc.Strand([scaf_ss1_left, scaf_ss0, scaf_ss1_right], color=sc.default_scaffold_color) + strands = [stap_left, stap_right, scaf] + design = sc.Design(helices=helices, strands=strands, grid=sc.square) + design.add_deletion(helix=0, offset=1) + design.add_deletion(helix=0, offset=4) + design.add_deletion(helix=1, offset=1) + design.add_deletion(helix=1, offset=4) + design.assign_dna(scaf, 'AACATCGT') + self.assertEqual("AACATCGT", scaf.dna_sequence) + self.assertEqual("TTTG", stap_left.dna_sequence) + self.assertEqual("GAAC", stap_right.dna_sequence) + + def test_assign_dna__wildcards_simple(self) -> None: + """ + 012 345 678 + -TTC> -GGA> -CCT> + None: + """ + 0 16 + AACGTATCGCGATGCATCC + 0 [-------I: 3-------> + <-------I: 3-------] + """ + design = sc.Design(grid=sc.square, helices=[sc.Helix(max_offset=100)], strands=[]) + design.draw_strand(0, 0).move(16) + design.draw_strand(0, 16).move(-16) + design.add_insertion(0, 8, 3) + design.assign_dna(strand=design.strands[0], sequence='AACGTATCGCGATGCATCC', assign_complement=True) + + """ + 0 16 + AACGTATCGCGATGCATCC + 0 [-------I: 3-------> + <-------I: 3-------] + TTGCATAGCGCTACGTAGG + """ + self.assertEqual(design.strands[0].dna_sequence, 'AACGTATCGCGATGCATCC') + self.assertEqual(design.strands[1].dna_sequence, 'GGATGCATCGCGATACGTT') + + def test_assign_dna__two_bound_strands__with_insertions__complement_true(self) -> None: + """ + 0 16 + + 0 [-------I: 3------> + <---]<--I: 3------] + TTGCATAGCGCTACGTAGG + """ + design = sc.Design(grid=sc.square, helices=[sc.Helix(max_offset=100)], strands=[]) + design.draw_strand(0, 0).move(16) + design.draw_strand(0, 5).move(-5) + design.draw_strand(0, 16).move(-11) + design.add_insertion(0, 8, 3) + design.assign_dna(strand=design.strands[1], sequence='ACGTT', assign_complement=True) + design.assign_dna(strand=design.strands[2], sequence='GGATGCATCGCGAT', assign_complement=True) + + """ + 0 16 + AACGTATCGCGATGCATCC + 0 [-------I: 3------> + <---]<--I: 3------] + TTGCATAGCGCTACGTAGG + """ + self.assertEqual(design.strands[0].dna_sequence, 'AACGTATCGCGATGCATCC') + self.assertEqual(design.strands[1].dna_sequence, 'ACGTT') + self.assertEqual(design.strands[2].dna_sequence, 'GGATGCATCGCGAT') + + def test_assign_dna__one_bound_strand__with_deletions__complement_true(self) -> None: + """ + 0 16 + AACGTACG TGCATCC + 0 [-------X------> + <-------X------] + """ + design = sc.Design(grid=sc.square, helices=[sc.Helix(max_offset=100)], strands=[]) + design.draw_strand(0, 0).move(16) + design.draw_strand(0, 16).move(-16) + design.add_deletion(0, 8) + design.assign_dna(strand=design.strands[0], sequence='AACGTACGTGCATCC', assign_complement=True) + + """ + 0 16 + AACGTACG TGCATCC + 0 [-------X------> + <-------X------] + TTGCATGC ACGTAGG + """ + self.assertEqual(design.strands[0].dna_sequence, 'AACGTACGTGCATCC') + self.assertEqual(design.strands[1].dna_sequence, 'GGATGCACGTACGTT') + + def test_assign_dna__two_bound_strands__with_deletions__complement_true(self) -> None: + """ + 0 16 + 0 [-------X------> + <---]<--X------] + TTGCATAG GCTACGT + """ + design = sc.Design(grid=sc.square, helices=[sc.Helix(max_offset=100)], strands=[]) + design.draw_strand(0, 0).move(16) + design.draw_strand(0, 5).move(-5) + design.draw_strand(0, 16).move(-11) + design.add_deletion(0, 8) + design.assign_dna(strand=design.strands[1], sequence='ACGTT', assign_complement=True) + design.assign_dna(strand=design.strands[2], sequence='TGCATCGGAT', assign_complement=True) + + """ + 0 16 + AACGTATC CGATGCA + 0 [-------X------> + <---]<--X------] + TTGCATAG GCTACGT + """ + self.assertEqual(design.strands[0].dna_sequence, 'AACGTATCCGATGCA') + self.assertEqual(design.strands[1].dna_sequence, 'ACGTT') + self.assertEqual(design.strands[2].dna_sequence, 'TGCATCGGAT') + + +class TestAssignDNAToDomains(unittest.TestCase): + + def setUp(self) -> None: + """ + 012 345 678 901 234 567 890 + +---------------+ + | | + | +-----|-------+ +-+ + | | | | | | + -ACG> +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA> + None: + """ + 012 345 678 901 234 567 890 + +---------------+ + | | + | +-----|-------+ +-+ + | | | | | | + -ACG> +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA> + None: + with self.assertRaises(sc.IllegalDesignError): + self.design.assign_dna(self.strand_top_big9, 'AACTTC', domain=self.dom_top9) + + def test_assign_dna__to_individual_domains__wildcards_multiple_overlaps(self) -> None: + """ + 012 345 678 901 234 567 890 + +---------------+ + | | + | +-----|-------+ +-+ + | | | | | | + -ACG> +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA> + +???> -???+ -AAC+ -???> +???+ +???> + +TTC> -???+ -AAC+ -???> +???+ +???> + +TTC> -???+ -AAC+ -TGC> +???+ +???> + +TTC> -???+ -AAC+ -TGC> +???+ +???> + +TTC> -???+ -AAC+ -TGC> +TTG+ +???> + +TTC> -???+ -AAC+ -TGC> +TTG+ +GCA> + +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA> + None: + """ + 012 345 678 901 234 567 890 + +---------------+ + | | + | +-----|-------+ +-+ + | | | | | | + -ACG> +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA> + + -TGC> + +TTC> -AAC+ -TGC> + +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA> + None: + ss0 = sc.Domain(0, True, 0, 10) + ss1 = sc.Domain(1, False, 0, 10) + strand = sc.Strand([ss0, ss1]) + strand.set_dna_sequence("AAAACCCCGGGGTTTTACGT") + # offset: 0 1 2 3 4 5 6 7 8 9 + # index: 0 1 2 3 4 5 6 7 8 9 + # A A A A C C C C G G + # helix 0 [ - - - - - - - - + + # | + # helix 1 < - - - - - - - - + + # T G C A T T T T G G + # offset: 0 1 2 3 4 5 6 7 8 9 + # index: 19 18 17 16 15 14 13 12 11 10 + self.assertEqual("A", ss0.dna_sequence_in(0, 0)) + self.assertEqual("AA", ss0.dna_sequence_in(0, 1)) + self.assertEqual("AAA", ss0.dna_sequence_in(0, 2)) + self.assertEqual("AAAA", ss0.dna_sequence_in(0, 3)) + self.assertEqual("AAAAC", ss0.dna_sequence_in(0, 4)) + self.assertEqual("AAAACC", ss0.dna_sequence_in(0, 5)) + self.assertEqual("AAAACCC", ss0.dna_sequence_in(0, 6)) + self.assertEqual("AAAACCCC", ss0.dna_sequence_in(0, 7)) + self.assertEqual("AAAACCCCG", ss0.dna_sequence_in(0, 8)) + self.assertEqual("AAAACCCCGG", ss0.dna_sequence_in(0, 9)) + # + self.assertEqual("G", ss1.dna_sequence_in(9, 9)) + self.assertEqual("GG", ss1.dna_sequence_in(8, 9)) + self.assertEqual("GGT", ss1.dna_sequence_in(7, 9)) + self.assertEqual("GGTT", ss1.dna_sequence_in(6, 9)) + self.assertEqual("GGTTT", ss1.dna_sequence_in(5, 9)) + self.assertEqual("GGTTTT", ss1.dna_sequence_in(4, 9)) + self.assertEqual("GGTTTTA", ss1.dna_sequence_in(3, 9)) + self.assertEqual("GGTTTTAC", ss1.dna_sequence_in(2, 9)) + self.assertEqual("GGTTTTACG", ss1.dna_sequence_in(1, 9)) + self.assertEqual("GGTTTTACGT", ss1.dna_sequence_in(0, 9)) + + def test_dna_sequence_in__right_then_left_deletions(self) -> None: + ss0 = sc.Domain(0, True, 0, 10, deletions=[2, 5, 6]) + ss1 = sc.Domain(1, False, 0, 10, deletions=[2, 6, 7]) + strand = sc.Strand([ss0, ss1]) + strand.set_dna_sequence("AAACCGGGGTTAGT") + # offset: 0 1 D2 3 4 D5 D6 7 8 9 + # index: 0 1 2 3 4 5 6 + # A A A C C G G + # helix 0 [ - - - - - - - - + + # | + # helix 1 < - - - - - - - - + + # T G A T T G G + # offset: 0 1 D2 3 4 5 D6 D7 8 9 + # index: 13 12 11 10 9 9 7 + self.assertEqual("A", ss0.dna_sequence_in(0, 0)) + self.assertEqual("AA", ss0.dna_sequence_in(0, 1)) + self.assertEqual("AA", ss0.dna_sequence_in(0, 2)) + self.assertEqual("AAA", ss0.dna_sequence_in(0, 3)) + self.assertEqual("AAAC", ss0.dna_sequence_in(0, 4)) + self.assertEqual("AAAC", ss0.dna_sequence_in(0, 5)) + self.assertEqual("AAAC", ss0.dna_sequence_in(0, 6)) + self.assertEqual("AAACC", ss0.dna_sequence_in(0, 7)) + self.assertEqual("AAACCG", ss0.dna_sequence_in(0, 8)) + self.assertEqual("AAACCGG", ss0.dna_sequence_in(0, 9)) + # + self.assertEqual("G", ss1.dna_sequence_in(9, 9)) + self.assertEqual("GG", ss1.dna_sequence_in(8, 9)) + self.assertEqual("GG", ss1.dna_sequence_in(7, 9)) + self.assertEqual("GG", ss1.dna_sequence_in(6, 9)) + self.assertEqual("GGT", ss1.dna_sequence_in(5, 9)) + self.assertEqual("GGTT", ss1.dna_sequence_in(4, 9)) + self.assertEqual("GGTTA", ss1.dna_sequence_in(3, 9)) + self.assertEqual("GGTTA", ss1.dna_sequence_in(2, 9)) + self.assertEqual("GGTTAG", ss1.dna_sequence_in(1, 9)) + self.assertEqual("GGTTAGT", ss1.dna_sequence_in(0, 9)) + + # if TEST_OFFSETS_AT_DELETION_INSERTIONS: + # self.assertEqual("AA", ss0.dna_sequence_in(0, 3)) + # self.assertEqual("AAACC", ss0.dna_sequence_in(0, 7)) + # self.assertEqual("GGT", ss1.dna_sequence_in(6, 10)) + # self.assertEqual("GGTTTA", ss1.dna_sequence_in(2, 10)) + + def test_dna_sequence_in__right_then_left_insertions(self) -> None: + ss0 = sc.Domain(0, True, 0, 10, insertions=[(2, 1), (6, 2)]) + ss1 = sc.Domain(1, False, 0, 10, insertions=[(2, 1), (6, 2)]) + strand = sc.Strand([ss0, ss1]) + strand.set_dna_sequence("AAAACCCCGGGGTTTTACGTACGTAC") + # offset: 0 1 2 I 3 4 5 6 I I 7 8 9 + # index: 0 1 2 3 4 5 6 7 8 9 10 11 12 + # A A A A C C C C G G G G T + # helix 0 [ - - - - - - - - - - - + + # | + # helix 1 < - - - - - - - - - - - + + # C A T G C A T G C A T T T + # offset: 0 1 2 I 3 4 5 6 I I 7 8 9 + # index: 25 24 23 22 21 20 19 18 17 16 15 14 13 + self.assertEqual("A", ss0.dna_sequence_in(0, 0)) + self.assertEqual("AA", ss0.dna_sequence_in(0, 1)) + self.assertEqual("AAAA", ss0.dna_sequence_in(0, 2)) + self.assertEqual("AAAAC", ss0.dna_sequence_in(0, 3)) + self.assertEqual("AAAACC", ss0.dna_sequence_in(0, 4)) + self.assertEqual("AAAACCC", ss0.dna_sequence_in(0, 5)) + self.assertEqual("AAAACCCCGG", ss0.dna_sequence_in(0, 6)) + self.assertEqual("AAAACCCCGGG", ss0.dna_sequence_in(0, 7)) + self.assertEqual("AAAACCCCGGGG", ss0.dna_sequence_in(0, 8)) + self.assertEqual("AAAACCCCGGGGT", ss0.dna_sequence_in(0, 9)) + # + self.assertEqual("T", ss1.dna_sequence_in(9, 9)) + self.assertEqual("TT", ss1.dna_sequence_in(8, 9)) + self.assertEqual("TTT", ss1.dna_sequence_in(7, 9)) + self.assertEqual("TTTACG", ss1.dna_sequence_in(6, 9)) + self.assertEqual("TTTACGT", ss1.dna_sequence_in(5, 9)) + self.assertEqual("TTTACGTA", ss1.dna_sequence_in(4, 9)) + self.assertEqual("TTTACGTAC", ss1.dna_sequence_in(3, 9)) + self.assertEqual("TTTACGTACGT", ss1.dna_sequence_in(2, 9)) + self.assertEqual("TTTACGTACGTA", ss1.dna_sequence_in(1, 9)) + self.assertEqual("TTTACGTACGTAC", ss1.dna_sequence_in(0, 9)) + + # if TEST_OFFSETS_AT_DELETION_INSERTIONS: + # self.assertEqual("AAAA", ss0.dna_sequence_in(0, 3)) + # self.assertEqual("AAAACCCCGG", ss0.dna_sequence_in(0, 7)) + # self.assertEqual("TTTACG", ss1.dna_sequence_in(6, 10)) + # self.assertEqual("TTTACGTACGT", ss1.dna_sequence_in(2, 10)) + + def test_dna_sequence_in__right_then_left_deletions_and_insertions(self) -> None: + ss0 = sc.Domain(0, True, 0, 10, deletions=[4], insertions=[(2, 1), (6, 2)]) + ss1 = sc.Domain(1, False, 0, 10, deletions=[4], insertions=[(2, 1), (6, 2)]) + strand = sc.Strand([ss0, ss1]) + strand.set_dna_sequence("AAAACCCCGGGGTTTTACGTACGT") + # offset: 0 1 2 I 3 D4 5 6 I I 7 8 9 + # index: 0 1 2 3 4 5 6 7 8 9 10 11 + # A A A A C C C C G G G G + # helix 0 [ - - - - - - - - - - - + + # | + # helix 1 < - - - - - - - - - - - + + # T G C A T G C A T T T T + # offset: 0 1 2 I 3 D4 5 6 I I 7 8 9 + # index: 23 22 21 20 19 18 17 16 15 14 13 12 + self.assertEqual("AA", ss0.dna_sequence_in(2, 2)) + self.assertEqual("CCG", ss0.dna_sequence_in(6, 6)) + self.assertEqual("TAC", ss1.dna_sequence_in(6, 6)) + self.assertEqual("AC", ss1.dna_sequence_in(2, 2)) + # + self.assertEqual("A ".strip(), ss0.dna_sequence_in(0, 0)) + self.assertEqual("AA ".strip(), ss0.dna_sequence_in(0, 1)) + self.assertEqual("AAAA ".strip(), ss0.dna_sequence_in(0, 2)) + self.assertEqual("AAAAC ".strip(), ss0.dna_sequence_in(0, 3)) + self.assertEqual("AAAAC ".strip(), ss0.dna_sequence_in(0, 4)) + self.assertEqual("AAAACC ".strip(), ss0.dna_sequence_in(0, 5)) + self.assertEqual("AAAACCCCG ".strip(), ss0.dna_sequence_in(0, 6)) + self.assertEqual("AAAACCCCGG ".strip(), ss0.dna_sequence_in(0, 7)) + self.assertEqual("AAAACCCCGGG ".strip(), ss0.dna_sequence_in(0, 8)) + self.assertEqual("AAAACCCCGGGG".strip(), ss0.dna_sequence_in(0, 9)) + self.assertEqual(" AAACCCCGGGG".strip(), ss0.dna_sequence_in(1, 9)) + self.assertEqual(" AACCCCGGGG".strip(), ss0.dna_sequence_in(2, 9)) + self.assertEqual(" CCCCGGGG".strip(), ss0.dna_sequence_in(3, 9)) + self.assertEqual(" CCCGGGG".strip(), ss0.dna_sequence_in(4, 9)) + self.assertEqual(" CCCGGGG".strip(), ss0.dna_sequence_in(5, 9)) + self.assertEqual(" CCGGGG".strip(), ss0.dna_sequence_in(6, 9)) + self.assertEqual(" GGG".strip(), ss0.dna_sequence_in(7, 9)) + self.assertEqual(" GG".strip(), ss0.dna_sequence_in(8, 9)) + self.assertEqual(" G".strip(), ss0.dna_sequence_in(9, 9)) + # strip() below is so auto-formatting preserves nice lineup of characters + self.assertEqual("T ".strip(), ss1.dna_sequence_in(9, 9)) + self.assertEqual("TT ".strip(), ss1.dna_sequence_in(8, 9)) + self.assertEqual("TTT ".strip(), ss1.dna_sequence_in(7, 9)) + self.assertEqual("TTTTAC ".strip(), ss1.dna_sequence_in(6, 9)) + self.assertEqual("TTTTACG ".strip(), ss1.dna_sequence_in(5, 9)) + self.assertEqual("TTTTACG ".strip(), ss1.dna_sequence_in(4, 9)) + self.assertEqual("TTTTACGT ".strip(), ss1.dna_sequence_in(3, 9)) + self.assertEqual("TTTTACGTAC ".strip(), ss1.dna_sequence_in(2, 9)) + self.assertEqual("TTTTACGTACG ".strip(), ss1.dna_sequence_in(1, 9)) + self.assertEqual("TTTTACGTACGT".strip(), ss1.dna_sequence_in(0, 9)) + self.assertEqual(" TTTACGTACGT".strip(), ss1.dna_sequence_in(0, 8)) + self.assertEqual(" TTACGTACGT".strip(), ss1.dna_sequence_in(0, 7)) + self.assertEqual(" TACGTACGT".strip(), ss1.dna_sequence_in(0, 6)) + self.assertEqual(" GTACGT".strip(), ss1.dna_sequence_in(0, 5)) + self.assertEqual(" TACGT".strip(), ss1.dna_sequence_in(0, 4)) + self.assertEqual(" TACGT".strip(), ss1.dna_sequence_in(0, 3)) + self.assertEqual(" ACGT".strip(), ss1.dna_sequence_in(0, 2)) + self.assertEqual(" GT".strip(), ss1.dna_sequence_in(0, 1)) + self.assertEqual(" T".strip(), ss1.dna_sequence_in(0, 0)) + + # if TEST_OFFSETS_AT_DELETION_INSERTIONS: + # self.assertEqual("AAAA", ss0.dna_sequence_in(0, 3)) + # self.assertEqual("AAAAC", ss0.dna_sequence_in(0, 5)) + # self.assertEqual("AAAACCCCGG", ss0.dna_sequence_in(0, 7)) + # self.assertEqual("TTTACG", ss1.dna_sequence_in(6, 10)) + # self.assertEqual("TTTTACG", ss1.dna_sequence_in(4, 10)) + # self.assertEqual("TTTACGTACGT", ss1.dna_sequence_in(2, 10)) + + +class TestOxviewExport(unittest.TestCase): + def test_export(self): + "Ensures that OxView export matches OxDNA export." + # Uses the basic design from OxdnaExport + helices = [sc.Helix(max_offset=7), sc.Helix(max_offset=7)] + design = sc.Design(helices=helices, grid=sc.square) + design.draw_strand(0, 0).move(7).cross(1).move(-7).with_color( + sc.Color(254, 123, 222)) + design.draw_strand(0, 7).move(-7).cross(1).move(7) + + oxdna_system = _convert_design_to_oxdna_system(design) + + oxv = design.to_oxview_format(use_strand_colors=True) + oxv_no_color = design.to_oxview_format(use_strand_colors=False) + + # Is the box correct? + self.assertEqual(list(oxdna_system.compute_bounding_box()), oxv['box']) + + # Do we have the same number of strands? + self.assertEqual(len(oxdna_system.strands), + len(oxv['systems'][0]['strands'])) + + for i, (oxdna_strand, oxview_strand, oxview_nocolor_strand, + des_strand) in enumerate( + zip(oxdna_system.strands, oxv['systems'][0]['strands'], + oxv_no_color['systems'][0]['strands'], + design.strands)): + self.assertEqual(i + 1, oxview_strand['id']) + + if des_strand.color: + scolor = des_strand.color.to_cadnano_v2_int_hex() + else: + scolor = None + + self.assertEqual(len(oxdna_strand.nucleotides), + len(oxview_strand['monomers'])) + for j, (oxdna_nt, oxview_nt, oxview_nocolor_nt) in enumerate( + zip(oxdna_strand.nucleotides, oxview_strand['monomers'], + oxview_nocolor_strand['monomers'])): + self.assertListEqual(list(oxdna_nt.r), oxview_nt['p']) + self.assertListEqual(list(oxdna_nt.b), oxview_nt['a1']) + self.assertListEqual(list(oxdna_nt.n), oxview_nt['a3']) + if scolor is not None: + self.assertEqual(scolor, oxview_nt['color']) + self.assertNotIn('color', oxview_nocolor_nt) + else: + self.assertNotIn('color', oxview_nt) + self.assertNotIn('color', oxview_nocolor_nt) + self.assertEqual(oxdna_nt.base, oxview_nt['type']) + + def test_bp(self): + des = sc.Design() + des.set_grid(sc.Grid.square) + des.helices = {i: sc.Helix(max_offset=20, idx=i, grid_position=(0, i)) for i in range(3)} + des.draw_strand(0, 0).to(6).with_deletions(4).to(15).cross(1, 9).to(20).with_insertions( + (15, 2)).cross(0).to(9) + des.draw_strand(1, 0).to(9).cross(0).to(0).with_deletions(4) + des.draw_strand(1, 20).to(2).with_insertions((15, 2)).cross(2, 0).to(20).with_sequence( + 'TTTCTCATGGGAAGCAAACTCGGTTTCCGCGTCGGATAGT') + des.draw_strand(2, 8).to(5).loopout(2, 5, 4).to(0) + des.draw_strand(2, 20).extension_5p(8).to(12).extension_3p(8).with_sequence( + 'ATACTGGAACTACGCGCGTGAATT', assign_complement=False) + + oxv = des.to_oxview_format() + + strands = oxv['systems'][0]['strands'] + + # Basic complements with a deletion (wildcard sequences) + for i in range(0, 8): + self.assertEqual(strands[0]['monomers'][i]['bp'], strands[1]['monomers'][-i - 1]['id']) + self.assertEqual(strands[1]['monomers'][-i - 1]['bp'], strands[0]['monomers'][i]['id']) + + # Self-complementary strand (wildcard sequences) + for i in range(8, 14): + self.assertEqual(strands[0]['monomers'][i]['bp'], strands[0]['monomers'][7 - i]['id']) + + # Insertion (defined sequences) + for i in range(14, 27): + self.assertEqual(strands[0]['monomers'][i]['bp'], strands[2]['monomers'][26 - i]['id']) + + # Before, in, and after a loopout (one strand with no sequence, one with defined sequence) + for i in range(0, 3): + self.assertEqual(strands[3]['monomers'][i]['bp'], strands[2]['monomers'][27 - i]['id']) + + for i in range(3, 8): + self.assertNotIn('bp', strands[3]['monomers'][i]) + + for i in range(8, 12): + self.assertEqual(strands[3]['monomers'][i]['bp'], strands[2]['monomers'][23 + 8 - i]['id']) + + # Mismatches should not be paired; also, extensions: + for i in range(0, 8): # 5p extension + self.assertNotIn('bp', strands[4]['monomers'][i]) + for i in range(8, 12): # complementary + print(i) + self.assertEqual(strands[4]['monomers'][i]['bp'], strands[2]['monomers'][40 + 7 - i]['id']) + for i in range(12, 14): # two mismatches + self.assertNotIn('bp', strands[4]['monomers'][i]) + self.assertNotIn('bp', strands[2]['monomers'][32 + 15 - i]) + for i in range(14, 16): # complementary again + self.assertEqual(strands[4]['monomers'][i]['bp'], strands[2]['monomers'][32 + 15 - i]['id']) + for i in range(16, len(strands[4]['monomers'])): # 3p extension + self.assertNotIn('bp', strands[4]['monomers'][i]) + + # Unbound region + for i in range(28, 32): + self.assertNotIn('bp', strands[2]['monomers'][i]) + + def test_export_file(self): + "Ensures that file export works, and writes a suitable JSON file that matches the output." + self.maxDiff = None + helices = [sc.Helix(max_offset=7), sc.Helix(max_offset=7)] + design = sc.Design(helices=helices, grid=sc.square) + design.draw_strand(0, 0).move(7).cross(1).move(-7).with_color( + sc.Color(254, 123, 222)) + design.draw_strand(0, 7).move(-7).cross(1).move(7) + + oxv = design.to_oxview_format(use_strand_colors=True) + + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + design.write_oxview_file(filename=f.name) + filename = f.name + with open(filename, 'r') as f2: + oxv2 = json.load(f2) + os.unlink(filename) + + # The dates won't be equal, so delete them + del oxv['date'] + del oxv2['date'] + + self.assertEqual(oxv, oxv2) + + +class TestOxdnaExport(unittest.TestCase): + def setUp(self) -> None: + self.OX_UNITS_TO_NM = 0.8518 + self.NM_TO_OX_UNITS = 1.0 / self.OX_UNITS_TO_NM + self.OX_BASE_DIST = 0.6 + self.BASES_PER_TURN = 10.5 + self.HELIX_ANGLE = math.pi * 2 / self.BASES_PER_TURN + self.RISE_PER_BASE_PAIR = 0.332 + # square of expected distance between adjacent nucleotide centers of mass + self.EXPECTED_ADJ_NUC_CM_DIST2 = (2 * self.OX_BASE_DIST * math.sin(self.HELIX_ANGLE / 2)) ** 2 + ( + self.RISE_PER_BASE_PAIR * self.NM_TO_OX_UNITS) ** 2 + + def test_basic_design(self) -> None: + """ 2 double strands of length 7 connected across helices. + 0 7 + helix 0 [------+ + +------]\ + | | + helix 1 +------>/ + <------+ + """ + helices = [sc.Helix(max_offset=7), sc.Helix(max_offset=7)] + design = sc.Design(helices=helices, grid=sc.square) + design.draw_strand(0, 0).move(7).cross(1).move(-7) + design.draw_strand(0, 7).move(-7).cross(1).move(7) + + # expected values for verification + expected_num_nucleotides = 7 * 4 + expected_strand_length = 7 * 2 + + dat, top = design.to_oxdna_format() + dat_lines = dat.strip().split('\n') + top_lines = top.strip().split('\n') + + # check length of output files are as expected (matches # of nucleotides plus header size) + self.assertEqual(expected_num_nucleotides + 3, len(dat_lines)) + self.assertEqual(expected_num_nucleotides + 1, len(top_lines)) + + # find relevant values for nucleotides + cm_poss = [] # center of mass position + nbrs_3p = [] + nbrs_5p = [] + + for line in dat_lines[3:]: + data = line.strip().split() + # make sure there are 15 values per line (3 values per vector * 5 vectors per line) + # order of vectors: center of mass position, backbone base versor, normal versor, velocity, angular velocity (more info on versors: https://eater.net/quaternions) + self.assertEqual(15, len(data)) + + cm_poss.append(tuple([float(x) for x in data[0:3]])) + bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector + nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector + + # make sure normal vectors and backbone vectors are unit length + sqr_bb_vec = sum([x ** 2 for x in bb_vec]) + sqr_nm_vec = sum([x ** 2 for x in nm_vec]) + self.assertAlmostEqual(1.0, sqr_bb_vec) + self.assertAlmostEqual(1.0, sqr_nm_vec) + + for value in data[9:]: # values for velocity and angular velocity vectors are 0 + self.assertAlmostEqual(0, float(value)) + + strand1_idxs = [] + strand2_idxs = [] + for nuc_idx, line in enumerate(top_lines[1:]): + data = line.strip().split() + # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor + self.assertEqual(4, len(data)) + + # make sure there are only 2 strands + strand_num = int(data[0]) + self.assertIn(strand_num, [1, 2]) + # make sure base is valid + base = data[1] + self.assertIn(base, ['A', 'C', 'G', 'T']) + + nbrs_3p.append(int(data[2])) + nbrs_5p.append(int(data[3])) + + # append start of strand (no 5' neighbor) to list of indexes for strand + neighbor_5 = int(data[3]) + if neighbor_5 == -1: + if strand_num == 1: + strand1_start = nuc_idx + strand1_idxs.append(strand1_start) + else: + strand2_start = nuc_idx + strand2_idxs.append(strand2_start) + + # reconstruct strands using indices from oxDNA files + next_idx = nbrs_3p[strand1_start] + while next_idx >= 0: + strand1_idxs.append(next_idx) + next_idx = nbrs_3p[strand1_idxs[-1]] + + next_idx = nbrs_3p[strand2_start] + while next_idx >= 0: + strand2_idxs.append(next_idx) + next_idx = nbrs_3p[strand2_idxs[-1]] + + # assert that strands are the correct length + self.assertEqual(expected_strand_length, len(strand1_idxs)) + self.assertEqual(expected_strand_length, len(strand2_idxs)) + + for i in range(expected_strand_length - 1): + # ignore nucleotide distance between domains (on crossover) + if i == 6: + continue + + strand1_nuc_idx1 = strand1_idxs[i] + strand1_nuc_idx2 = strand1_idxs[i + 1] + strand2_nuc_idx1 = strand2_idxs[i] + strand2_nuc_idx2 = strand2_idxs[i + 1] + + # find the center of mass for adjacent nucleotides + s1_cmp1 = cm_poss[strand1_nuc_idx1] + s1_cmp2 = cm_poss[strand1_nuc_idx2] + s2_cmp1 = cm_poss[strand2_nuc_idx1] + s2_cmp2 = cm_poss[strand2_nuc_idx2] + + # calculate and verify squared distance between adjacent nucleotides in a domain + diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) + diff2 = tuple([s2_cmp1[j] - s2_cmp2[j] for j in range(3)]) + sqr_dist1 = sum([x ** 2 for x in diff1]) + sqr_dist2 = sum([x ** 2 for x in diff2]) + + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist2) + + def test_helix_groups(self) -> None: + """ + 2 double strands of length 7 connected across helices. + + honeycomb square + group a group b + + 01234567 01234567 + helix 0 [------+ helix 2 + | +------] + | | + helix 1 | helix 3 +------> + <------+ + + + Other than placing second strand on new helices in new helix group, same design as basic design, + so testing the same things essentially. + """ + helices = [sc.Helix(max_offset=7, group='a'), sc.Helix(max_offset=7, group='a'), + sc.Helix(max_offset=7, group='b'), sc.Helix(max_offset=7, group='b')] + groups = { + 'a': sc.HelixGroup(position=sc.Position3D(0, 0, 0), grid=sc.honeycomb), + 'b': sc.HelixGroup(position=sc.Position3D(100, 0, 0), grid=sc.square), + } + design = sc.Design(helices=helices, groups=groups) + design.draw_strand(0, 0).move(7).cross(1).move(-7) + design.draw_strand(2, 7).move(-7).cross(3).move(7) # unlike basic design, put strand on helices 2,3 + + # expected values for verification + expected_num_nucleotides = 7 * 4 + expected_strand_length = 7 * 2 + + dat, top = design.to_oxdna_format() + dat_lines = dat.strip().split('\n') + top_lines = top.strip().split('\n') + + # check length of output files are as expected (matches # of nucleotides plus header size) + self.assertEqual(expected_num_nucleotides + 3, len(dat_lines)) + self.assertEqual(expected_num_nucleotides + 1, len(top_lines)) + + # find relevant values for nucleotides + cm_poss = [] # center of mass position + nbrs_3p = [] + nbrs_5p = [] + + for line in dat_lines[3:]: + data = line.strip().split() + # make sure there are 15 values per line (3 values per vector * 5 vectors per line) + # order of vectors: center of mass position, backbone base versor, normal versor, velocity, angular velocity (more info on versors: https://eater.net/quaternions) + self.assertEqual(15, len(data)) + + cm_poss.append(tuple([float(x) for x in data[0:3]])) + bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector + nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector + + # make sure normal vectors and backbone vectors are unit length + sqr_bb_vec = sum([x ** 2 for x in bb_vec]) + sqr_nm_vec = sum([x ** 2 for x in nm_vec]) + self.assertAlmostEqual(1.0, sqr_bb_vec) + self.assertAlmostEqual(1.0, sqr_nm_vec) + + for value in data[9:]: # values for velocity and angular velocity vectors are 0 + self.assertAlmostEqual(0, float(value)) + + strand1_idxs = [] + strand2_idxs = [] + for nuc_idx, line in enumerate(top_lines[1:]): + data = line.strip().split() + # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor + self.assertEqual(4, len(data)) + + # make sure there are only 2 strands + strand_num = int(data[0]) + self.assertIn(strand_num, [1, 2]) + # make sure base is valid + base = data[1] + self.assertIn(base, ['A', 'C', 'G', 'T']) + + nbrs_3p.append(int(data[2])) + nbrs_5p.append(int(data[3])) + + # append start of strand (no 5' neighbor) to list of indexes for strand + neighbor_5 = int(data[3]) + if neighbor_5 == -1: + if strand_num == 1: + strand1_start = nuc_idx + strand1_idxs.append(strand1_start) + else: + strand2_start = nuc_idx + strand2_idxs.append(strand2_start) + + # reconstruct strands using indices from oxDNA files + next_idx = nbrs_3p[strand1_start] + while next_idx >= 0: + strand1_idxs.append(next_idx) + next_idx = nbrs_3p[strand1_idxs[-1]] + + next_idx = nbrs_3p[strand2_start] + while next_idx >= 0: + strand2_idxs.append(next_idx) + next_idx = nbrs_3p[strand2_idxs[-1]] + + # assert that strands are the correct length + self.assertEqual(expected_strand_length, len(strand1_idxs)) + self.assertEqual(expected_strand_length, len(strand2_idxs)) + + for i in range(expected_strand_length - 1): + # ignore nucleotide distance between domains (on crossover) + if i == 6: + continue + + strand1_nuc_idx1 = strand1_idxs[i] + strand1_nuc_idx2 = strand1_idxs[i + 1] + strand2_nuc_idx1 = strand2_idxs[i] + strand2_nuc_idx2 = strand2_idxs[i + 1] + + # find the center of mass for adjacent nucleotides + s1_cmp1 = cm_poss[strand1_nuc_idx1] + s1_cmp2 = cm_poss[strand1_nuc_idx2] + s2_cmp1 = cm_poss[strand2_nuc_idx1] + s2_cmp2 = cm_poss[strand2_nuc_idx2] + + # calculate and verify squared distance between adjacent nucleotides in a domain + diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) + diff2 = tuple([s2_cmp1[j] - s2_cmp2[j] for j in range(3)]) + sqr_dist1 = sum([x ** 2 for x in diff1]) + sqr_dist2 = sum([x ** 2 for x in diff2]) + + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist2) + + def test_honeycomb_design(self) -> None: + """ A single strand on a honeycomb grid. + 0 8 + helix 0 [-------+ + | + helix 1 +-------- + | + helix 2 +-------> + """ + helices = [sc.Helix(grid_position=(1, 1), max_offset=8), sc.Helix(grid_position=(0, 1), max_offset=8), + sc.Helix(grid_position=(0, 2), max_offset=8)] + design = sc.Design(helices=helices, grid=sc.honeycomb) + design.draw_strand(0, 0).to(8).cross(1).move(-8).cross(2).to(8) + + # expected values for verification + expected_num_nucleotides = 8 * 3 + expected_strand_length = 8 * 3 + + dat, top = design.to_oxdna_format() + dat = dat.strip().split('\n') + top = top.strip().split('\n') + + # check length of output files are as expected (matches # of nucleotides plus header size) + self.assertEqual(expected_num_nucleotides + 3, len(dat)) + self.assertEqual(expected_num_nucleotides + 1, len(top)) + + # find relevant values for nucleotides + cm_poss = [] # center of mass position + nbrs_3p = [] + nbrs_5p = [] + + for line in dat[3:]: + data = line.strip().split() + # make sure there are 15 values per line (3 values per vector * 5 vectors per line) + # order of vectors: center of mass position, backbone base, normal, velocity, angular velocity + self.assertEqual(15, len(data)) + + cm_poss.append(tuple([float(x) for x in data[0:3]])) + bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector + nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector + + # make sure normal vectors and backbone vectors are unit length + sqr_bb_vec = sum([x ** 2 for x in bb_vec]) + sqr_nm_vec = sum([x ** 2 for x in nm_vec]) + self.assertAlmostEqual(1.0, sqr_bb_vec) + self.assertAlmostEqual(1.0, sqr_nm_vec) + + for value in data[9:]: # values for velocity and angular velocity vectors are 0 + self.assertAlmostEqual(0, float(value)) + + strand1_idxs = [] + for nuc_idx, line in enumerate(top[1:]): + data = line.strip().split() + # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor + self.assertEqual(4, len(data)) + + # make sure there's only 1 strand + strand_num = int(data[0]) + self.assertEqual(1, strand_num) + # make sure base is valid + base = data[1] + self.assertIn(base, ['A', 'C', 'G', 'T']) + + nbrs_3p.append(int(data[2])) + nbrs_5p.append(int(data[3])) + + # append start of strand (no 5' neighbor) to list of indexes for strand + neighbor_5 = int(data[3]) + if neighbor_5 == -1: + strand1_start = nuc_idx + strand1_idxs.append(strand1_start) + + # reconstruct strand using indices from oxDNA files + next_idx = nbrs_3p[strand1_start] + while next_idx >= 0: + strand1_idxs.append(next_idx) + next_idx = nbrs_3p[strand1_idxs[-1]] + + # assert that strand is correct length + self.assertEqual(expected_strand_length, len(strand1_idxs)) + + for i in range(expected_strand_length - 1): + # ignore nucleotide distances between domains (on crossovers) + if i == 7: + continue + + if i == 15: + continue + + strand1_nuc_idx1 = strand1_idxs[i] + strand1_nuc_idx2 = strand1_idxs[i + 1] + + # find the center of mass for adjacent nucleotides + s1_cmp1 = cm_poss[strand1_nuc_idx1] + s1_cmp2 = cm_poss[strand1_nuc_idx2] + + # calculate and verify squared distance between adjacent nucleotides in a domain + diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) + sqr_dist1 = sum([x ** 2 for x in diff1]) + + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) + + def test_deletion_design(self) -> None: + """ + 0 7 + [---X--> + """ + helix = [sc.Helix(max_offset=7)] + design = sc.Design(helices=helix, grid=sc.square) + design.draw_strand(0, 0).to(7) + design.add_deletion(helix=0, offset=4) + + # expected values for verification + expected_num_nucleotides = 6 + expected_strand_length = 6 + + dat, top = design.to_oxdna_format() + dat = dat.strip().split('\n') + top = top.strip().split('\n') + + # check length of output files are as expected (matches # of nucleotides plus header size) + self.assertEqual(expected_num_nucleotides + 3, len(dat)) + self.assertEqual(expected_num_nucleotides + 1, len(top)) + + # find relevant values for nucleotides + cm_poss = [] # center of mass position + nbrs_3p = [] + nbrs_5p = [] + + for line in dat[3:]: + data = line.strip().split() + # make sure there are 15 values per line (3 values per vector * 5 vectors per line) + # order of vectors: center of mass position, backbone base, normal, velocity, angular velocity + self.assertEqual(15, len(data)) + + cm_poss.append(tuple([float(x) for x in data[0:3]])) + bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector + nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector + + # make sure normal vectors and backbone vectors are unit length + sqr_bb_vec = sum([x ** 2 for x in bb_vec]) + sqr_nm_vec = sum([x ** 2 for x in nm_vec]) + self.assertAlmostEqual(1.0, sqr_bb_vec) + self.assertAlmostEqual(1.0, sqr_nm_vec) + + for value in data[9:]: # values for velocity and angular velocity vectors are 0 + self.assertAlmostEqual(0, float(value)) + + strand1_idxs = [] + for nuc_idx, line in enumerate(top[1:]): + data = line.strip().split() + # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor + self.assertEqual(4, len(data)) + + # make sure there's only 1 strand + strand_num = int(data[0]) + self.assertEqual(1, strand_num) + # make sure base is valid + base = data[1] + self.assertIn(base, ['A', 'C', 'G', 'T']) + + nbrs_3p.append(int(data[2])) + nbrs_5p.append(int(data[3])) + + # append start of strand (no 5' neighbor) to list of indexes for strand + neighbor_5 = int(data[3]) + if neighbor_5 == -1: + strand1_start = nuc_idx + strand1_idxs.append(strand1_start) + + # reconstruct strand using indices from oxDNA files + next_idx = nbrs_3p[strand1_start] + while next_idx >= 0: + strand1_idxs.append(next_idx) + next_idx = nbrs_3p[strand1_idxs[-1]] + + # assert that strand is correct length + self.assertEqual(expected_strand_length, len(strand1_idxs)) + + for i in range(expected_strand_length - 1): + strand1_nuc_idx1 = strand1_idxs[i] + strand1_nuc_idx2 = strand1_idxs[i + 1] + + # find the center of mass for adjacent nucleotides + s1_cmp1 = cm_poss[strand1_nuc_idx1] + s1_cmp2 = cm_poss[strand1_nuc_idx2] + + # calculate and verify squared distance between adjacent nucleotides in a domain + diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) + sqr_dist1 = sum([x ** 2 for x in diff1]) + + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) + + def test_insertion_design(self) -> None: + """ + 0 7 + ^ insertion of length 1 + [------> + """ + helix = [sc.Helix(max_offset=10)] + design = sc.Design(helices=helix, grid=sc.square) + design.draw_strand(0, 0).to(7) + design.add_insertion(helix=0, offset=4, length=1) + + # expected values for verification + expected_num_nucleotides = 8 + expected_strand_length = 8 + + dat, top = design.to_oxdna_format() + dat = dat.strip().split('\n') + top = top.strip().split('\n') + + # check length of output files are as expected (matches # of nucleotides plus header size) + self.assertEqual(expected_num_nucleotides + 3, len(dat)) + self.assertEqual(expected_num_nucleotides + 1, len(top)) + + # find relevant values for nucleotides + cm_poss = [] # center of mass position + nbrs_3p = [] + nbrs_5p = [] + + for line in dat[3:]: + data = line.strip().split() + # make sure there are 15 values per line (3 values per vector * 5 vectors per line) + # order of vectors: center of mass position, backbone base, normal, velocity, angular velocity + self.assertEqual(15, len(data)) + + cm_poss.append(tuple([float(x) for x in data[0:3]])) + bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector + nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector + + # make sure normal vectors and backbone vectors are unit length + sqr_bb_vec = sum([x ** 2 for x in bb_vec]) + sqr_nm_vec = sum([x ** 2 for x in nm_vec]) + self.assertAlmostEqual(1.0, sqr_bb_vec) + self.assertAlmostEqual(1.0, sqr_nm_vec) + + for value in data[9:]: # values for velocity and angular velocity vectors are 0 + self.assertAlmostEqual(0, float(value)) + + strand1_idxs = [] + for nuc_idx, line in enumerate(top[1:]): + data = line.strip().split() + # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor + self.assertEqual(4, len(data)) + + # make sure there's only 1 strand + strand_num = int(data[0]) + self.assertEqual(1, strand_num) + # make sure base is valid + base = data[1] + self.assertIn(base, ['A', 'C', 'G', 'T']) + + nbrs_3p.append(int(data[2])) + nbrs_5p.append(int(data[3])) + + # append start of strand (no 5' neighbor) to list of indexes for strand + neighbor_5 = int(data[3]) + if neighbor_5 == -1: + strand1_start = nuc_idx + strand1_idxs.append(strand1_start) + + # reconstruct strand using indices from oxDNA files + next_idx = nbrs_3p[strand1_start] + while next_idx >= 0: + strand1_idxs.append(next_idx) + next_idx = nbrs_3p[strand1_idxs[-1]] + + # assert that strand is correct length + self.assertEqual(expected_strand_length, len(strand1_idxs)) + + for i in range(expected_strand_length - 1): + strand1_nuc_idx1 = strand1_idxs[i] + strand1_nuc_idx2 = strand1_idxs[i + 1] + + # find the center of mass for adjacent nucleotides + s1_cmp1 = cm_poss[strand1_nuc_idx1] + s1_cmp2 = cm_poss[strand1_nuc_idx2] + + # calculate and verify squared distance between adjacent nucleotides in a domain + diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) + sqr_dist1 = sum([x ** 2 for x in diff1]) + + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) + + def test_loopout_design(self) -> None: + """ 2 strands, one with a loopout + 0 7 + ^ loopout at 4 of length = 4 bases + [------> + <------] + """ + helix = [sc.Helix(max_offset=14)] + design = sc.Design(helices=helix, grid=sc.square) + design.draw_strand(0, 0).to(4).loopout(0, 4).to(7) + design.draw_strand(0, 7).to(0) + + # expected values for verification + expected_num_nucleotides = 7 * 2 + 4 + expected_strand_1_length = 7 + 4 # strand 1 has loopout of 4 + expected_strand_2_length = 7 + + dat, top = design.to_oxdna_format() + dat = dat.strip().split('\n') + top = top.strip().split('\n') + + # check length of output files are as expected (matches # of nucleotides plus header size) + self.assertEqual(expected_num_nucleotides + 3, len(dat)) + self.assertEqual(expected_num_nucleotides + 1, len(top)) + + # find relevant values for nucleotides + cm_poss = [] # center of mass position + nbrs_3p = [] + nbrs_5p = [] + + for line in dat[3:]: + data = line.strip().split() + # make sure there are 15 values per line (3 values per vector * 5 vectors per line) + # order of vectors: center of mass position, backbone base, normal, velocity, angular velocity + self.assertEqual(15, len(data)) + + cm_poss.append(tuple([float(x) for x in data[0:3]])) + bb_vec = tuple([float(x) for x in data[3:6]]) # backbone base vector + nm_vec = tuple([float(x) for x in data[6:9]]) # normal vector + + # make sure normal vectors and backbone vectors are unit length + sqr_bb_vec = sum([x ** 2 for x in bb_vec]) + sqr_nm_vec = sum([x ** 2 for x in nm_vec]) + self.assertAlmostEqual(1.0, sqr_bb_vec) + self.assertAlmostEqual(1.0, sqr_nm_vec) + + for value in data[9:]: # values for velocity and angular velocity vectors are 0 + self.assertAlmostEqual(0, float(value)) + + strand1_idxs = [] + strand2_idxs = [] + for nuc_idx, line in enumerate(top[1:]): + data = line.strip().split() + # make sure there are 4 values per line: strand, base, 3' neighbor, 5' neighbor + self.assertEqual(4, len(data)) + + # make sure there are only 2 strands + strand_num = int(data[0]) + self.assertIn(strand_num, [1, 2]) + # make sure base is valid + base = data[1] + self.assertIn(base, ['A', 'C', 'G', 'T']) + + nbrs_3p.append(int(data[2])) + nbrs_5p.append(int(data[3])) + + # append start of strand (no 5' neighbor) to list of indexes for strand + neighbor_5 = int(data[3]) + if neighbor_5 == -1: + if strand_num == 1: + strand1_start = nuc_idx + strand1_idxs.append(strand1_start) + else: + strand2_start = nuc_idx + strand2_idxs.append(strand2_start) + + # reconstruct strands using indices from oxDNA files + next_idx = nbrs_3p[strand1_start] + while next_idx >= 0: + strand1_idxs.append(next_idx) + next_idx = nbrs_3p[strand1_idxs[-1]] + + next_idx = nbrs_3p[strand2_start] + while next_idx >= 0: + strand2_idxs.append(next_idx) + next_idx = nbrs_3p[strand2_idxs[-1]] + + # assert that strands are the correct length + self.assertEqual(expected_strand_1_length, len(strand1_idxs)) + self.assertEqual(expected_strand_2_length, len(strand2_idxs)) + + # calculate distance between nucleotides preceding and following loopout and confirm that it matches expected distance between nucleotides. + # loopout here is nucleotides 4 to 7, so check distance between nucleotides 3 to 8 + cm_poss_pre_loopout = cm_poss[strand1_idxs[3]] + cm_poss_post_loopout = cm_poss[strand1_idxs[8]] + diff = tuple([cm_poss_pre_loopout[j] - cm_poss_post_loopout[j] for j in range(3)]) + sqr_dist = sum([x ** 2 for x in diff]) + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist) + + for i in range(expected_strand_1_length - 1): + + if i in [3, 4, 5, 6, + 7]: # skip nucleotide distances having to do with loopout, as these won't have regular distance between nucleotides (i = 3 denotes distance from 3 to 4, which includes loopout) + continue + + strand1_nuc_idx1 = strand1_idxs[i] + strand1_nuc_idx2 = strand1_idxs[i + 1] + + # find the center of mass for adjacent nucleotides + s1_cmp1 = cm_poss[strand1_nuc_idx1] + s1_cmp2 = cm_poss[strand1_nuc_idx2] + + # calculate and verify squared distance between adjacent nucleotides in a domain + diff1 = tuple([s1_cmp1[j] - s1_cmp2[j] for j in range(3)]) + sqr_dist1 = sum([x ** 2 for x in diff1]) + + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist1) + + for i in range(expected_strand_2_length - 1): # check adjacent nucleotide distances for second strand + strand2_nuc_idx1 = strand2_idxs[i] + strand2_nuc_idx2 = strand2_idxs[i + 1] + + # find the center of mass for adjacent nucleotides + s2_cmp1 = cm_poss[strand2_nuc_idx1] + s2_cmp2 = cm_poss[strand2_nuc_idx2] + + # calculate and verify squared distance between adjacent nucleotides in a domain + diff2 = tuple([s2_cmp1[j] - s2_cmp2[j] for j in range(3)]) + sqr_dist2 = sum([x ** 2 for x in diff2]) + + self.assertAlmostEqual(self.EXPECTED_ADJ_NUC_CM_DIST2, sqr_dist2) + + def test_file_output(self) -> None: + # Arbitrary design + helix = [sc.Helix(max_offset=14)] + design = sc.Design(helices=helix, grid=sc.square) + design.draw_strand(0, 0).to(4).loopout(0, 4).to(7) + design.draw_strand(0, 7).to(0) + + scriptname = os.path.basename(sys.argv[0])[:-3] + + dat, top = design.to_oxdna_format() + + with tempfile.TemporaryDirectory() as tmpdir: + # First, write to the directory, in which case the names should be the script name + design.write_oxdna_files(directory=tmpdir) + self.assertEqual(top, open(tmpdir + '/' + scriptname + '.top').read()) + self.assertEqual(dat, open(tmpdir + '/' + scriptname + '.dat').read()) + + # Now, write, to a specific filename without extensions + design.write_oxdna_files(directory=tmpdir, + filename_no_extension='oxdna-Export with spaces in name') + self.assertEqual(top, open(tmpdir + '/oxdna-Export with spaces in name.top').read()) + self.assertEqual(dat, open(tmpdir + '/oxdna-Export with spaces in name.dat').read()) + + +class TestPlateMaps(unittest.TestCase): + + def setUp(self) -> None: + helices = [sc.Helix(max_offset=100)] + self.design = sc.Design(helices=helices, strands=[], grid=sc.square) + self.design.draw_strand(0, 0).move(10).with_name('strand 0').with_idt(plate='plate 1', well='A1') + self.design.draw_strand(0, 10).move(10).with_name('strand 1').with_idt(plate='plate 1', well='A2') + self.design.draw_strand(0, 20).move(10).with_name('strand 2').with_idt(plate='plate 1', well='B2') + self.design.draw_strand(0, 30).move(10).with_name('strand 3').with_idt(plate='plate 1', well='B3') + self.design.draw_strand(0, 40).move(10).with_name('strand 4').with_idt(plate='plate 1', well='D7') + + def test_plate_map_markdown(self) -> None: + plate_maps = self.design.plate_maps() + self.assertEqual(1, len(plate_maps)) + plate_map = plate_maps[0] + actual_md = plate_map.to_table().strip() + expected_md = """ +### plate "plate 1" +| | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | +|:----|:---------|:---------|:---------|:----|:----|:----|:---------|:----|:----|:-----|:-----|:-----| +| A | strand 0 | strand 1 | | | | | | | | | | | +| B | | strand 2 | strand 3 | | | | | | | | | | +| C | | | | | | | | | | | | | +| D | | | | | | | strand 4 | | | | | | +| E | | | | | | | | | | | | | +| F | | | | | | | | | | | | | +| G | | | | | | | | | | | | | +| H | | | | | | | | | | | | | +""".strip() + self.assertEqual(expected_md, actual_md) + + +class TestExtension(unittest.TestCase): + def test_to_json_serializable__extension_key_contains_num_bases(self) -> None: + ext = sc.Extension(5) + result = ext.to_json_serializable(False) + self.assertEqual(result[sc.extension_key], 5) + + def test_to_json_serializable__no_display_length_key_when_default_display_length(self) -> None: + ext = sc.Extension(5) + result = ext.to_json_serializable(False) + self.assertNotIn(sc.display_length_key, result) + + def test_to_json_serializable__no_display_angle_key_when_default_display_angle(self) -> None: + ext = sc.Extension(5) + result = ext.to_json_serializable(False) + self.assertNotIn(sc.display_angle_key, result) + + def test_to_json_serializable__no_name_key_when_default_name(self) -> None: + ext = sc.Extension(5) + result = ext.to_json_serializable(False) + self.assertNotIn(sc.domain_name_key, result) + + def test_to_json_serializable__no_label_key_when_default_label(self) -> None: + ext = sc.Extension(5) + result = ext.to_json_serializable(False) + self.assertNotIn(sc.domain_label_key, result) + + def test_to_json_serializable__display_length_key_contains_non_default_display_length(self) -> None: + ext = sc.Extension(5, display_length=1.9) + result = ext.to_json_serializable(False) + self.assertEqual(result[sc.display_length_key], 1.9) + + def test_to_json_serializable__display_angle_key_contains_non_default_display_angle(self) -> None: + ext = sc.Extension(5, display_angle=39.9) + result = ext.to_json_serializable(False) + self.assertEqual(result[sc.display_angle_key], 39.9) + + def test_to_json_serializable__name_key_contains_non_default_name(self) -> None: + ext = sc.Extension(5, name="A") + result = ext.to_json_serializable(False) + self.assertEqual(result[sc.domain_name_key], "A") + + def test_to_json_serializable__label_key_contains_non_default_name(self) -> None: + ext = sc.Extension(5, label="ext1") + result = ext.to_json_serializable(False) + self.assertEqual(result[sc.domain_label_key], "ext1") + + +class TestBasePairs(unittest.TestCase): + def setUp(self) -> None: + ''' + X shows position of mismatches + 111111111122222222223333333333 + 0123456789012345678901234567890123456789 + 0 [-->[--> [--> [--> [--> + <] <--------] <-] <-] <] <] + X + 111111111122222222223333333333 + 0123456789012345678901234567890123456789 + 1 [-----------> [--> + <--] <-----------] + X + ''' + helices = [sc.Helix(max_offset=40) for _ in range(2)] + self.design = sc.Design(helices=helices) + # helix 0 forward + self.design.draw_strand(0, 0).move(4).with_sequence('AAAA') + self.design.draw_strand(0, 4).move(4).with_sequence('AAAA') + self.design.draw_strand(0, 12).move(4).with_sequence('AAAA') + self.design.draw_strand(0, 20).move(4).with_sequence('AAAA') + self.design.draw_strand(0, 28).move(4).with_sequence('AAAA') + # helix 0 reverse + self.design.draw_strand(0, 3).move(-2).with_sequence('TT') + self.design.draw_strand(0, 14).to(4).with_sequence('TTTTTTTTCT') + self.design.draw_strand(0, 20).to(17).with_sequence('TTT') + self.design.draw_strand(0, 29).to(26).with_sequence('TTT') + self.design.draw_strand(0, 36).to(34).with_sequence('TT') + self.design.draw_strand(0, 39).to(37).with_sequence('TT') + # helix 1 forward + self.design.draw_strand(1, 4).to(17).with_sequence('A' * 13) + self.design.draw_strand(1, 20).to(24).with_sequence('A' * 4) + # helix 1 reverse + self.design.draw_strand(1, 12).to(8).with_sequence('TGTT') + self.design.draw_strand(1, 26).to(13).with_sequence('T' * 13) + + def test_find_overlapping_domains(self) -> None: + d01f = self.design.strands[0].domains[0] + d02f = self.design.strands[1].domains[0] + d03f = self.design.strands[2].domains[0] + d04f = self.design.strands[3].domains[0] + d05f = self.design.strands[4].domains[0] + + d01r = self.design.strands[5].domains[0] + d02r = self.design.strands[6].domains[0] + d03r = self.design.strands[7].domains[0] + d04r = self.design.strands[8].domains[0] + d05r = self.design.strands[9].domains[0] + d06r = self.design.strands[10].domains[0] + + d11f = self.design.strands[11].domains[0] + d12f = self.design.strands[12].domains[0] + + d11r = self.design.strands[13].domains[0] + d12r = self.design.strands[14].domains[0] + + overlapping_domains_h0 = sc.find_overlapping_domains_on_helix(self.design.helices[0]) + overlapping_domains_h1 = sc.find_overlapping_domains_on_helix(self.design.helices[1]) + + self.assertEqual(len(overlapping_domains_h0), 4) + self.assertEqual(len(overlapping_domains_h1), 3) + + self.assertIn((d01f, d01r), overlapping_domains_h0) + self.assertIn((d02f, d02r), overlapping_domains_h0) + self.assertIn((d03f, d02r), overlapping_domains_h0) + self.assertIn((d05f, d04r), overlapping_domains_h0) + + self.assertIn((d11f, d11r), overlapping_domains_h1) + self.assertIn((d11f, d12r), overlapping_domains_h1) + self.assertIn((d12f, d12r), overlapping_domains_h1) + ''' + 111111111122222222223333333333 + 0123456789012345678901234567890123456789 + 0 [-->[--> [--> [--> [--> + <] <--------] <-] <-] <] <] + + 111111111122222222223333333333 + 0123456789012345678901234567890123456789 + 1 [-----------> [--> + <--] <-----------] + ''' + + def test_design_base_pairs_mismatches(self) -> None: + base_pairs = self.design.base_pairs(allow_mismatches=True) + self.assertEqual(len(base_pairs), 2) + self.assertEqual(len(base_pairs[0]), 9) + self.assertEqual(len(base_pairs[1]), 12) + + # d01f, d01r + self.assertIn(1, base_pairs[0]) + self.assertIn(2, base_pairs[0]) + + # d02f, d02r + self.assertIn(4, base_pairs[0]) + self.assertIn(5, base_pairs[0]) + self.assertIn(6, base_pairs[0]) + self.assertIn(7, base_pairs[0]) + + # d03f, d02r + self.assertIn(12, base_pairs[0]) + self.assertIn(13, base_pairs[0]) + + # d05f, d04r + self.assertIn(28, base_pairs[0]) + + # d11f, d11r + self.assertIn(8, base_pairs[1]) + self.assertIn(9, base_pairs[1]) + self.assertIn(10, base_pairs[1]) + self.assertIn(11, base_pairs[1]) + + # d11f, d12r + self.assertIn(13, base_pairs[1]) + self.assertIn(14, base_pairs[1]) + self.assertIn(15, base_pairs[1]) + self.assertIn(16, base_pairs[1]) + + # d12f, d12r + self.assertIn(20, base_pairs[1]) + self.assertIn(21, base_pairs[1]) + self.assertIn(22, base_pairs[1]) + self.assertIn(23, base_pairs[1]) + ''' + 111111111122222222223333333333 + 0123456789012345678901234567890123456789 + 0 [-->[--> [--> [--> [--> + <] <--------] <-] <-] <] <] + + 111111111122222222223333333333 + 0123456789012345678901234567890123456789 + 1 [-----------> [--> + <--] <-----------] + ''' + + def test_design_base_pairs_no_mismatches(self) -> None: + base_pairs = self.design.base_pairs(allow_mismatches=False) + self.assertEqual(len(base_pairs), 2) + self.assertEqual(len(base_pairs[0]), 8) + self.assertEqual(len(base_pairs[1]), 11) + + # d01f, d01r + self.assertIn(1, base_pairs[0]) + self.assertIn(2, base_pairs[0]) + + # d02f, d02r + self.assertIn(4, base_pairs[0]) + # self.assertIn(5, base_pairs[0]) # mismatch + self.assertIn(6, base_pairs[0]) + self.assertIn(7, base_pairs[0]) + + # d03f, d02r + self.assertIn(12, base_pairs[0]) + self.assertIn(13, base_pairs[0]) + + # d05f, d04r + self.assertIn(28, base_pairs[0]) + + # d11f, d11r + self.assertIn(8, base_pairs[1]) + self.assertIn(9, base_pairs[1]) + # self.assertIn(10, base_pairs[1]) # mismatch + self.assertIn(11, base_pairs[1]) + + # d11f, d12r + self.assertIn(13, base_pairs[1]) + self.assertIn(14, base_pairs[1]) + self.assertIn(15, base_pairs[1]) + self.assertIn(16, base_pairs[1]) + + # d12f, d12r + self.assertIn(20, base_pairs[1]) + self.assertIn(21, base_pairs[1]) + self.assertIn(22, base_pairs[1]) + self.assertIn(23, base_pairs[1]) + ''' + X shows position of mismatches + 111111111122222222223333333333 + 0123456789012345678901234567890123456789 + 0 [-->[--> [--> [--> [--> + <] <--------] <-] <-] <] <] + X + 111111111122222222223333333333 + 0123456789012345678901234567890123456789 + 1 [-----------> [--> + <--] <-----------] + X + ''' + + def test_design_base_pairs_no_dna(self) -> None: + ''' + 0123456789 + 0 [--------> + <---]<---] + ''' + design = sc.Design(helices=[sc.Helix(max_offset=100)]) + design.draw_strand(0, 0).move(10) + design.draw_strand(0, 5).move(-5) + design.draw_strand(0, 10).move(-5) + + base_pairs = design.base_pairs() + self.assertEqual(len(base_pairs), 1) + self.assertEqual(len(base_pairs[0]), 10) + + for offset in range(10): + self.assertIn(offset, base_pairs[0]) + + def test_design_base_pairs_dna_on_some_strands_and_mismatches(self) -> None: + ''' + 0123456789 + AAAAAAAAAA + 0 [--------> + <---]<---] + TTCTT + ''' + design = sc.Design(helices=[sc.Helix(max_offset=100)]) + design.draw_strand(0, 0).move(10).with_sequence('A' * 10) + design.draw_strand(0, 5).move(-5).with_sequence('TTCTT') + design.draw_strand(0, 10).move(-5) + + base_pairs = design.base_pairs(allow_mismatches=False) + self.assertEqual(len(base_pairs), 1) + self.assertEqual(len(base_pairs[0]), 9) + + for offset in range(10): + if offset != 2: + self.assertIn(offset, base_pairs[0]) + + def test_design_base_pairs_deletions_insertions(self) -> None: + ''' + 0123456789 + AA + A AAAAAAA + 0 [XX---II-> + <-XX]<-II] + TT TTTTTT + TT + ''' + design = sc.Design(helices=[sc.Helix(max_offset=100)]) + design.draw_strand(0, 0).move(10).with_deletions([1, 2]).with_insertions([(6, 1), (7, 1)]) \ + .with_sequence('A' * 10) + design.draw_strand(0, 5).move(-5).with_deletions([2, 3]).with_sequence('TTT') + design.draw_strand(0, 10).move(-5).with_insertions([(7, 1), (8, 1)]).with_sequence('T' * 7) + + base_pairs = design.base_pairs(allow_mismatches=False) + self.assertEqual(len(base_pairs), 1) + self.assertEqual(len(base_pairs[0]), 5) + + self.assertIn(0, base_pairs[0]) + self.assertIn(4, base_pairs[0]) + self.assertIn(5, base_pairs[0]) + self.assertIn(7, base_pairs[0]) + self.assertIn(9, base_pairs[0]) + + def test_design_base_pairs_deletions_insertions_mismatch_in_insertion(self) -> None: + ''' + 0123456789 + AA + A AAAAAAA + 0 [XX---II-> + <-XX]<-II] + TT TTTTTT + CT + ''' + design = sc.Design(helices=[sc.Helix(max_offset=100)]) + design.draw_strand(0, 0).move(10).with_deletions([1, 2]).with_insertions([(6, 1), (7, 1)]) \ + .with_sequence('A' * 10) + design.draw_strand(0, 5).move(-5).with_deletions([2, 3]).with_sequence('TTT') + design.draw_strand(0, 10).move(-5).with_insertions([(7, 1), (8, 1)]).with_sequence('TTTCTTT') + + base_pairs = design.base_pairs(allow_mismatches=False) + self.assertEqual(len(base_pairs), 1) + self.assertEqual(len(base_pairs[0]), 4) + + self.assertIn(0, base_pairs[0]) + self.assertIn(4, base_pairs[0]) + self.assertIn(5, base_pairs[0]) + self.assertIn(9, base_pairs[0]) + + def test_no_base_pairs(self) -> None: + ''' + 0123456789 + [--> + <--] + ''' + design = sc.Design(helices=[sc.Helix(max_offset=100)]) + design.draw_strand(0, 0).move(4) + design.draw_strand(0, 9).move(-4) + + base_pairs = design.base_pairs(allow_mismatches=False) + self.assertEqual(len(base_pairs), 0) + + def test_no_base_pairs_only_forward_strand(self) -> None: + ''' + 0123456789 + [--> + ''' + design = sc.Design(helices=[sc.Helix(max_offset=100)]) + design.draw_strand(0, 0).move(4) + + base_pairs = design.base_pairs(allow_mismatches=False) + self.assertEqual(len(base_pairs), 0) + + def test_no_base_pairs_only_reverse_strand(self) -> None: + ''' + 0123456789 + <--] + ''' + design = sc.Design(helices=[sc.Helix(max_offset=100)]) + design.draw_strand(0, 8).move(-4) + + base_pairs = design.base_pairs(allow_mismatches=False) + self.assertEqual(len(base_pairs), 0) + + def test_base_pairs_on_forward_strand_ahead_of_reverse_strand(self) -> None: + ''' + 0123456789 + [----> + <----] + ''' + design = sc.Design(helices=[sc.Helix(max_offset=100)]) + design.draw_strand(0, 2).move(6) + design.draw_strand(0, 6).move(-6) + + base_pairs = design.base_pairs(allow_mismatches=False) + self.assertEqual(len(base_pairs), 1) + self.assertEqual(len(base_pairs[0]), 4) + + self.assertIn(2, base_pairs[0]) + self.assertIn(3, base_pairs[0]) + self.assertIn(4, base_pairs[0]) + self.assertIn(5, base_pairs[0])