Repository: UC-Davis-molecular-computing/scadnano-python-package (language: python)

Issue:
change label type to string
Scadnano supports the notion of a `label` attached to a strand, domain, loopout, or extension.

Currently, the type of `label` is an arbitrary JSON-serializable object. However, this makes type checking awkward, and since such objects can be mutable, the web interface (which relies on immutable state date) might have unforeseen problems due to this.

It would be simpler to simply say the type of `label` is a string. If a user wants more structured data, they can do the JSON serialization and deserialization themselves. Note that this will be a breaking change.

See https://github.com/UC-Davis-molecular-computing/scadnano/issues/878


Discussion / hints:

Fix patch:
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 <https://docs.python.org/3/library/json.html#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 <https://docs.python.org/3/library/json.html#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 <https://docs.python.org/3/library/json.html#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"""\
     <style>
     th.{cell_with_border_css_class}, td.{cell_with_border_css_class} {{
         border: 1px solid black;
     }}
     </style>
     <table>\
-    """,
-            "",
-            "",
-            "",
-        ),
+    """, "", "", "", )
+    html_with_borders_tablefmt = TableFormat(  # type: ignore
+        lineabove=lineabove,
         linebelowheader=None,
         linebetweenrows=None,
         linebelow=Line("</table>", "", "", ""),
@@ -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)


Test patch:
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>
-        <AAA---CCC---GGG-
-         876   543   210
-        """
-        ss_bot = sc.Domain(helix=0, forward=False, start=0, end=9)
-        ss_top1 = sc.Domain(helix=0, forward=True, start=0, end=3)
-        ss_top2 = sc.Domain(helix=0, forward=True, start=3, end=6)
-        ss_top3 = sc.Domain(helix=0, forward=True, start=6, end=9)
-        strand_bot = sc.Strand(domains=[ss_bot])
-        strand_top1 = sc.Strand(domains=[ss_top1])
-        strand_top2 = sc.Strand(domains=[ss_top2])
-        strand_top3 = sc.Strand(domains=[ss_top3])
-        strands = [strand_bot, strand_top1, strand_top2, strand_top3]
-        design = sc.Design(grid=sc.square, strands=strands)
-        design.assign_dna(strand_bot, 'AAACCCGGG')
-        self.assertEqual('CCC', strand_top1.dna_sequence)
-        self.assertEqual('GGG', strand_top2.dna_sequence)
-        self.assertEqual('TTT', strand_top3.dna_sequence)
-
-    def test_assign_dna__two_helices_with_multiple_domain_intersections(self) -> 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 <ACATAAGAAAACGGAG--+
-        M13   +-TGTATTCTTTTGCCTC> |
-              |                   |
-              +-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>
-        <AAG---CCT---GGA- 
-         876   543   210
-        """
-        ss_bot = sc.Domain(helix=0, forward=False, start=0, end=9)
-        ss_top1 = sc.Domain(helix=0, forward=True, start=0, end=3)
-        ss_top2 = sc.Domain(helix=0, forward=True, start=3, end=6)
-        ss_top3 = sc.Domain(helix=0, forward=True, start=6, end=9)
-        strand_bot = sc.Strand(domains=[ss_bot])
-        strand_top1 = sc.Strand(domains=[ss_top1])
-        strand_top2 = sc.Strand(domains=[ss_top2])
-        strand_top3 = sc.Strand(domains=[ss_top3])
-        strands = [strand_bot, strand_top1, strand_top2, strand_top3]
-        design = sc.Design(grid=sc.square, strands=strands)
-
-        design.assign_dna(strand_top1, 'TTC')
-        self.assertEqual('??????GAA', strand_bot.dna_sequence)
-
-        design.assign_dna(strand_top3, 'CCT')
-        self.assertEqual('AGG???GAA', strand_bot.dna_sequence)
-
-        design.assign_dna(strand_top2, 'GGA')
-        self.assertEqual('AGGTCCGAA', strand_bot.dna_sequence)
-
-    def test_assign_dna__one_bound_strand__with_insertions__complement_true(self) -> 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>
-        <TGC---AAG---CCT---TTG---ACG---AAC---CGT-
-         098   765   432   109   876   543   210
-        """
-        self.dom_bot = sc.Domain(helix=0, forward=False, start=0, end=21)
-        self.dom_top0 = sc.Domain(helix=0, forward=True, start=0, end=3)
-        self.dom_top3 = sc.Domain(helix=0, forward=True, start=3, end=6)
-        self.dom_top6 = sc.Domain(helix=0, forward=True, start=6, end=9)
-        self.dom_top9 = sc.Domain(helix=0, forward=True, start=9, end=12)
-        self.dom_top12 = sc.Domain(helix=0, forward=True, start=12, end=15)
-        self.dom_top15 = sc.Domain(helix=0, forward=True, start=15, end=18)
-        self.dom_top18 = sc.Domain(helix=0, forward=True, start=18, end=21)
-        self.strand_bot = sc.Strand(domains=[self.dom_bot])
-        self.strand_top_small0 = sc.Strand(domains=[self.dom_top0])
-        self.strand_top_small12 = sc.Strand(domains=[self.dom_top12])
-        self.strand_top_big9 = sc.Strand(domains=[self.dom_top9, self.dom_top3])
-        self.strand_top_big6 = sc.Strand(domains=[self.dom_top6, self.dom_top15, self.dom_top18])
-        strands = [self.strand_bot, self.strand_top_small0, self.strand_top_small12,
-                   self.strand_top_big9, self.strand_top_big6]
-        self.design = sc.Design(grid=sc.square, strands=strands)
-
-    def test_assign_dna__wildcards_multiple_overlaps(self) -> None:
-        """
-         012   345   678   901   234   567   890
-              +---------------+
-              |               |
-              |         +-----|-------+   +-+
-              |         |     |       |   | |
-        -ACG> +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA>
-        <TGC---AAG---CCT---TTG---ACG---AAC---CGT-
-         098   765   432   109   876   543   210
-        """
-        self.design.assign_dna(self.strand_top_big9, 'AACTTC')
-        self.assertEqual('??? ??? ??? GTT ??? GAA ???'.replace(' ', ''), self.strand_bot.dna_sequence)
-
-        self.design.assign_dna(self.strand_top_small12, 'TGC')
-        self.assertEqual('??? ??? GCA GTT ??? GAA ???'.replace(' ', ''), self.strand_bot.dna_sequence)
-
-        self.design.assign_dna(self.strand_top_small0, 'ACG')
-        self.assertEqual('??? ??? GCA GTT ??? GAA CGT'.replace(' ', ''), self.strand_bot.dna_sequence)
-
-        self.design.assign_dna(self.strand_top_big6, 'GGATTGGCA')
-        self.assertEqual('TGC CAA GCA GTT TCC GAA CGT'.replace(' ', ''), self.strand_bot.dna_sequence)
-
-    def test_assign_dna__domain_sequence_too_long_error(self) -> 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>
-        <TGC---AAG---CCT---TTG---ACG---AAC---CGT-
-         098   765   432   109   876   543   210
-        """
-
-        """
-         012   345   678   901   234   567   890
-              +---------------+
-              |               |
-              |         +-----|-------+   +-+
-              |         |     |       |   | |
-        -???> +???> -???+ -AAC+ -???> +???+ +???>
-        <???---???---???---TTG---???---???---???-
-         098   765   432   109   876   543   210
-        """
-        self.design.assign_dna(self.strand_top_big9, 'AAC', domain=self.dom_top9)
-        self.assertEqual('AAC ???'.replace(' ', ''), self.strand_top_big9.dna_sequence)
-        self.assertEqual('??? ??? ??? GTT ??? ??? ???'.replace(' ', ''), self.strand_bot.dna_sequence)
-
-        """
-         012   345   678   901   234   567   890
-              +---------------+
-              |               |
-              |         +-----|-------+   +-+
-              |         |     |       |   | |
-        -???> +TTC> -???+ -AAC+ -???> +???+ +???>
-        <???---AAG---???---TTG---???---???---???-
-         098   765   432   109   876   543   210
-        """
-        self.design.assign_dna(self.strand_top_big9, 'TTC', domain=self.dom_top3)
-        self.assertEqual('AAC TTC'.replace(' ', ''), self.strand_top_big9.dna_sequence)
-        self.assertEqual('??? ??? ??? GTT ??? GAA ???'.replace(' ', ''), self.strand_bot.dna_sequence)
-
-        """
-         012   345   678   901   234   567   890
-              +---------------+
-              |               |
-              |         +-----|-------+   +-+
-              |         |     |       |   | |
-        -???> +TTC> -???+ -AAC+ -TGC> +???+ +???>
-        <???---AAG---???---TTG---ACG---???---???-
-         098   765   432   109   876   543   210
-        """
-        self.design.assign_dna(self.strand_top_small12, 'TGC')
-        self.assertEqual('TGC', self.strand_top_small12.dna_sequence)
-        self.assertEqual('??? ??? GCA GTT ??? GAA ???'.replace(' ', ''), self.strand_bot.dna_sequence)
-
-        """
-         012   345   678   901   234   567   890
-              +---------------+
-              |               |
-              |         +-----|-------+   +-+
-              |         |     |       |   | |
-        -ACG> +TTC> -???+ -AAC+ -TGC> +???+ +???>
-        <TGC---AAG---???---TTG---ACG---???---???-
-         098   765   432   109   876   543   210
-        """
-        self.design.assign_dna(self.strand_top_small0, 'ACG')
-        self.assertEqual('ACG', self.strand_top_small0.dna_sequence)
-        self.assertEqual('??? ??? GCA GTT ??? GAA CGT'.replace(' ', ''), self.strand_bot.dna_sequence)
-
-        """
-         012   345   678   901   234   567   890
-              +---------------+
-              |               |
-              |         +-----|-------+   +-+
-              |         |     |       |   | |
-        -ACG> +TTC> -???+ -AAC+ -TGC> +TTG+ +???>
-        <TGC---AAG---???---TTG---ACG---AAC---???-
-         098   765   432   109   876   543   210
-        """
-        self.design.assign_dna(self.strand_top_big6, 'TTG', domain=self.dom_top15)
-        self.assertEqual('??? TTG ???'.replace(' ', ''), self.strand_top_big6.dna_sequence)
-        self.assertEqual('??? CAA GCA GTT ??? GAA CGT'.replace(' ', ''), self.strand_bot.dna_sequence)
-
-        """
-         012   345   678   901   234   567   890
-              +---------------+
-              |               |
-              |         +-----|-------+   +-+
-              |         |     |       |   | |
-        -ACG> +TTC> -???+ -AAC+ -TGC> +TTG+ +GCA>
-        <TGC---AAG---???---TTG---ACG---AAC---CGT-
-         098   765   432   109   876   543   210
-        """
-        self.design.assign_dna(self.strand_top_big6, 'GCA', domain=self.dom_top18)
-        self.assertEqual('??? TTG GCA'.replace(' ', ''), self.strand_top_big6.dna_sequence)
-        self.assertEqual('TGC CAA GCA GTT ??? GAA CGT'.replace(' ', ''), self.strand_bot.dna_sequence)
-
-        """
-         012   345   678   901   234   567   890
-              +---------------+
-              |               |
-              |         +-----|-------+   +-+
-              |         |     |       |   | |
-        -ACG> +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA>
-        <TGC---AAG---CCT---TTG---ACG---AAC---CGT-
-         098   765   432   109   876   543   210
-        """
-        self.design.assign_dna(self.strand_top_big6, 'GGA', domain=self.dom_top6)
-        self.assertEqual('GGA TTG GCA'.replace(' ', ''), self.strand_top_big6.dna_sequence)
-        self.assertEqual('TGC CAA GCA GTT TCC GAA CGT'.replace(' ', ''), self.strand_bot.dna_sequence)
-
-    def test_method_chaining_with_domain_sequence(self) -> None:
-        """
-         012   345   678   901   234   567   890
-              +---------------+
-              |               |
-              |         +-----|-------+   +-+
-              |         |     |       |   | |
-        -ACG> +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA>
-        <TGC---AAG---CCT---TTG---ACG---AAC---CGT-
-         098   765   432   109   876   543   210
-        """
-        design = sc.Design(grid=sc.square, helices=[sc.Helix(max_offset=100)], strands=[])
-
-        """
-         012   345   678   901   234   567   890
-        
-        <???---???---???---???---???---???---???-
-         098   765   432   109   876   543   210
-        """
-        design.draw_strand(0, 21).to(0)
-        self.assertEqual(1, len(design.strands))
-        self.assertEqual(21, design.strands[0].domains[0].dna_length())
-
-        """
-         012   345   678   901   234   567   890
-        
-                                -TGC>
-        <???---???---???---???---ACG---???---???-
-         098   765   432   109   876   543   210
-        """
-        design.draw_strand(0, 12).to(15).with_sequence('TGC', assign_complement=True)
-        self.assertEqual('TGC'.replace(' ', ''), design.strands[-1].dna_sequence)
-        self.assertEqual('??? ??? GCA ??? ??? ??? ???'.replace(' ', ''), design.strands[0].dna_sequence)
-
-        """
-         012   345   678   901   234   567   890
-        
-        -ACG>                   -TGC>
-        <???---???---???---???---ACG---???---???-
-         098   765   432   109   876   543   210
-        """
-        design.draw_strand(0, 0).to(3).with_sequence('ACG', assign_complement=False)
-        self.assertEqual('ACG'.replace(' ', ''), design.strands[-1].dna_sequence)
-        self.assertEqual('??? ??? GCA ??? ??? ??? ???'.replace(' ', ''), design.strands[0].dna_sequence)
-
-        """
-         012   345   678   901   234   567   890
-              +---------------+
-              |               |
-              |               |
-              |               |
-        -ACG> +TTC>       -AAC+ -TGC>
-        <???---AAG---???---TTG---ACG---???---???-
-         098   765   432   109   876   543   210
-        """
-        sb = design.draw_strand(0, 9).to(12)
-        sb.with_domain_sequence('AAC', assign_complement=True)
-        sb.cross(0, offset=3)
-        sb.to(6)
-        sb.with_domain_sequence('TTC', assign_complement=True)
-        self.assertEqual('AAC TTC'.replace(' ', ''), design.strands[-1].dna_sequence)
-        self.assertEqual('??? ??? GCA GTT ??? GAA ???'.replace(' ', ''), design.strands[0].dna_sequence)
-
-        """
-         012   345   678   901   234   567   890
-              +---------------+
-              |               |
-              |         +-----|-------+   +-+
-              |         |     |       |   | |
-        -ACG> +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA>
-        <???---AAG---CCT---TTG---ACG---AAC---CGT-
-         098   765   432   109   876   543   210
-        """
-        design.draw_strand(0, 6).to(9) \
-            .with_domain_sequence('GGA', assign_complement=True) \
-            .cross(0, offset=15) \
-            .to(18) \
-            .with_domain_sequence('TTG', assign_complement=True) \
-            .to(21) \
-            .with_domain_sequence('GCA', assign_complement=True)
-        self.assertEqual('GGA TTG GCA'.replace(' ', ''), design.strands[-1].dna_sequence)
-        self.assertEqual('TGC CAA GCA GTT TCC GAA ???'.replace(' ', ''), design.strands[0].dna_sequence)
-
-
-TEST_OFFSETS_AT_DELETION_INSERTIONS = False
-
-
-class TestSubstrandDNASequenceIn(unittest.TestCase):
-
-    def test_dna_sequence_in__right_then_left(self) -> 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>
+        <AAA---CCC---GGG-
+         876   543   210
+        """
+        ss_bot = sc.Domain(helix=0, forward=False, start=0, end=9)
+        ss_top1 = sc.Domain(helix=0, forward=True, start=0, end=3)
+        ss_top2 = sc.Domain(helix=0, forward=True, start=3, end=6)
+        ss_top3 = sc.Domain(helix=0, forward=True, start=6, end=9)
+        strand_bot = sc.Strand(domains=[ss_bot])
+        strand_top1 = sc.Strand(domains=[ss_top1])
+        strand_top2 = sc.Strand(domains=[ss_top2])
+        strand_top3 = sc.Strand(domains=[ss_top3])
+        strands = [strand_bot, strand_top1, strand_top2, strand_top3]
+        design = sc.Design(grid=sc.square, strands=strands)
+        design.assign_dna(strand_bot, 'AAACCCGGG')
+        self.assertEqual('CCC', strand_top1.dna_sequence)
+        self.assertEqual('GGG', strand_top2.dna_sequence)
+        self.assertEqual('TTT', strand_top3.dna_sequence)
+
+    def test_assign_dna__two_helices_with_multiple_domain_intersections(self) -> 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 <ACATAAGAAAACGGAG--+
+        M13   +-TGTATTCTTTTGCCTC> |
+              |                   |
+              +-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>
+        <AAG---CCT---GGA- 
+         876   543   210
+        """
+        ss_bot = sc.Domain(helix=0, forward=False, start=0, end=9)
+        ss_top1 = sc.Domain(helix=0, forward=True, start=0, end=3)
+        ss_top2 = sc.Domain(helix=0, forward=True, start=3, end=6)
+        ss_top3 = sc.Domain(helix=0, forward=True, start=6, end=9)
+        strand_bot = sc.Strand(domains=[ss_bot])
+        strand_top1 = sc.Strand(domains=[ss_top1])
+        strand_top2 = sc.Strand(domains=[ss_top2])
+        strand_top3 = sc.Strand(domains=[ss_top3])
+        strands = [strand_bot, strand_top1, strand_top2, strand_top3]
+        design = sc.Design(grid=sc.square, strands=strands)
+
+        design.assign_dna(strand_top1, 'TTC')
+        self.assertEqual('??????GAA', strand_bot.dna_sequence)
+
+        design.assign_dna(strand_top3, 'CCT')
+        self.assertEqual('AGG???GAA', strand_bot.dna_sequence)
+
+        design.assign_dna(strand_top2, 'GGA')
+        self.assertEqual('AGGTCCGAA', strand_bot.dna_sequence)
+
+    def test_assign_dna__one_bound_strand__with_insertions__complement_true(self) -> 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>
+        <TGC---AAG---CCT---TTG---ACG---AAC---CGT-
+         098   765   432   109   876   543   210
+        """
+        self.dom_bot = sc.Domain(helix=0, forward=False, start=0, end=21)
+        self.dom_top0 = sc.Domain(helix=0, forward=True, start=0, end=3)
+        self.dom_top3 = sc.Domain(helix=0, forward=True, start=3, end=6)
+        self.dom_top6 = sc.Domain(helix=0, forward=True, start=6, end=9)
+        self.dom_top9 = sc.Domain(helix=0, forward=True, start=9, end=12)
+        self.dom_top12 = sc.Domain(helix=0, forward=True, start=12, end=15)
+        self.dom_top15 = sc.Domain(helix=0, forward=True, start=15, end=18)
+        self.dom_top18 = sc.Domain(helix=0, forward=True, start=18, end=21)
+        self.strand_bot = sc.Strand(domains=[self.dom_bot])
+        self.strand_top_small0 = sc.Strand(domains=[self.dom_top0])
+        self.strand_top_small12 = sc.Strand(domains=[self.dom_top12])
+        self.strand_top_big9 = sc.Strand(domains=[self.dom_top9, self.dom_top3])
+        self.strand_top_big6 = sc.Strand(domains=[self.dom_top6, self.dom_top15, self.dom_top18])
+        strands = [self.strand_bot, self.strand_top_small0, self.strand_top_small12,
+                   self.strand_top_big9, self.strand_top_big6]
+        self.design = sc.Design(grid=sc.square, strands=strands)
+
+    def test_assign_dna__wildcards_multiple_overlaps(self) -> None:
+        """
+         012   345   678   901   234   567   890
+              +---------------+
+              |               |
+              |         +-----|-------+   +-+
+              |         |     |       |   | |
+        -ACG> +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA>
+        <TGC---AAG---CCT---TTG---ACG---AAC---CGT-
+         098   765   432   109   876   543   210
+        """
+        self.design.assign_dna(self.strand_top_big9, 'AACTTC')
+        self.assertEqual('??? ??? ??? GTT ??? GAA ???'.replace(' ', ''), self.strand_bot.dna_sequence)
+
+        self.design.assign_dna(self.strand_top_small12, 'TGC')
+        self.assertEqual('??? ??? GCA GTT ??? GAA ???'.replace(' ', ''), self.strand_bot.dna_sequence)
+
+        self.design.assign_dna(self.strand_top_small0, 'ACG')
+        self.assertEqual('??? ??? GCA GTT ??? GAA CGT'.replace(' ', ''), self.strand_bot.dna_sequence)
+
+        self.design.assign_dna(self.strand_top_big6, 'GGATTGGCA')
+        self.assertEqual('TGC CAA GCA GTT TCC GAA CGT'.replace(' ', ''), self.strand_bot.dna_sequence)
+
+    def test_assign_dna__domain_sequence_too_long_error(self) -> 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>
+        <TGC---AAG---CCT---TTG---ACG---AAC---CGT-
+         098   765   432   109   876   543   210
+        """
+
+        """
+         012   345   678   901   234   567   890
+              +---------------+
+              |               |
+              |         +-----|-------+   +-+
+              |         |     |       |   | |
+        -???> +???> -???+ -AAC+ -???> +???+ +???>
+        <???---???---???---TTG---???---???---???-
+         098   765   432   109   876   543   210
+        """
+        self.design.assign_dna(self.strand_top_big9, 'AAC', domain=self.dom_top9)
+        self.assertEqual('AAC ???'.replace(' ', ''), self.strand_top_big9.dna_sequence)
+        self.assertEqual('??? ??? ??? GTT ??? ??? ???'.replace(' ', ''), self.strand_bot.dna_sequence)
+
+        """
+         012   345   678   901   234   567   890
+              +---------------+
+              |               |
+              |         +-----|-------+   +-+
+              |         |     |       |   | |
+        -???> +TTC> -???+ -AAC+ -???> +???+ +???>
+        <???---AAG---???---TTG---???---???---???-
+         098   765   432   109   876   543   210
+        """
+        self.design.assign_dna(self.strand_top_big9, 'TTC', domain=self.dom_top3)
+        self.assertEqual('AAC TTC'.replace(' ', ''), self.strand_top_big9.dna_sequence)
+        self.assertEqual('??? ??? ??? GTT ??? GAA ???'.replace(' ', ''), self.strand_bot.dna_sequence)
+
+        """
+         012   345   678   901   234   567   890
+              +---------------+
+              |               |
+              |         +-----|-------+   +-+
+              |         |     |       |   | |
+        -???> +TTC> -???+ -AAC+ -TGC> +???+ +???>
+        <???---AAG---???---TTG---ACG---???---???-
+         098   765   432   109   876   543   210
+        """
+        self.design.assign_dna(self.strand_top_small12, 'TGC')
+        self.assertEqual('TGC', self.strand_top_small12.dna_sequence)
+        self.assertEqual('??? ??? GCA GTT ??? GAA ???'.replace(' ', ''), self.strand_bot.dna_sequence)
+
+        """
+         012   345   678   901   234   567   890
+              +---------------+
+              |               |
+              |         +-----|-------+   +-+
+              |         |     |       |   | |
+        -ACG> +TTC> -???+ -AAC+ -TGC> +???+ +???>
+        <TGC---AAG---???---TTG---ACG---???---???-
+         098   765   432   109   876   543   210
+        """
+        self.design.assign_dna(self.strand_top_small0, 'ACG')
+        self.assertEqual('ACG', self.strand_top_small0.dna_sequence)
+        self.assertEqual('??? ??? GCA GTT ??? GAA CGT'.replace(' ', ''), self.strand_bot.dna_sequence)
+
+        """
+         012   345   678   901   234   567   890
+              +---------------+
+              |               |
+              |         +-----|-------+   +-+
+              |         |     |       |   | |
+        -ACG> +TTC> -???+ -AAC+ -TGC> +TTG+ +???>
+        <TGC---AAG---???---TTG---ACG---AAC---???-
+         098   765   432   109   876   543   210
+        """
+        self.design.assign_dna(self.strand_top_big6, 'TTG', domain=self.dom_top15)
+        self.assertEqual('??? TTG ???'.replace(' ', ''), self.strand_top_big6.dna_sequence)
+        self.assertEqual('??? CAA GCA GTT ??? GAA CGT'.replace(' ', ''), self.strand_bot.dna_sequence)
+
+        """
+         012   345   678   901   234   567   890
+              +---------------+
+              |               |
+              |         +-----|-------+   +-+
+              |         |     |       |   | |
+        -ACG> +TTC> -???+ -AAC+ -TGC> +TTG+ +GCA>
+        <TGC---AAG---???---TTG---ACG---AAC---CGT-
+         098   765   432   109   876   543   210
+        """
+        self.design.assign_dna(self.strand_top_big6, 'GCA', domain=self.dom_top18)
+        self.assertEqual('??? TTG GCA'.replace(' ', ''), self.strand_top_big6.dna_sequence)
+        self.assertEqual('TGC CAA GCA GTT ??? GAA CGT'.replace(' ', ''), self.strand_bot.dna_sequence)
+
+        """
+         012   345   678   901   234   567   890
+              +---------------+
+              |               |
+              |         +-----|-------+   +-+
+              |         |     |       |   | |
+        -ACG> +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA>
+        <TGC---AAG---CCT---TTG---ACG---AAC---CGT-
+         098   765   432   109   876   543   210
+        """
+        self.design.assign_dna(self.strand_top_big6, 'GGA', domain=self.dom_top6)
+        self.assertEqual('GGA TTG GCA'.replace(' ', ''), self.strand_top_big6.dna_sequence)
+        self.assertEqual('TGC CAA GCA GTT TCC GAA CGT'.replace(' ', ''), self.strand_bot.dna_sequence)
+
+    def test_method_chaining_with_domain_sequence(self) -> None:
+        """
+         012   345   678   901   234   567   890
+              +---------------+
+              |               |
+              |         +-----|-------+   +-+
+              |         |     |       |   | |
+        -ACG> +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA>
+        <TGC---AAG---CCT---TTG---ACG---AAC---CGT-
+         098   765   432   109   876   543   210
+        """
+        design = sc.Design(grid=sc.square, helices=[sc.Helix(max_offset=100)], strands=[])
+
+        """
+         012   345   678   901   234   567   890
+        
+        <???---???---???---???---???---???---???-
+         098   765   432   109   876   543   210
+        """
+        design.draw_strand(0, 21).to(0)
+        self.assertEqual(1, len(design.strands))
+        self.assertEqual(21, design.strands[0].domains[0].dna_length())
+
+        """
+         012   345   678   901   234   567   890
+        
+                                -TGC>
+        <???---???---???---???---ACG---???---???-
+         098   765   432   109   876   543   210
+        """
+        design.draw_strand(0, 12).to(15).with_sequence('TGC', assign_complement=True)
+        self.assertEqual('TGC'.replace(' ', ''), design.strands[-1].dna_sequence)
+        self.assertEqual('??? ??? GCA ??? ??? ??? ???'.replace(' ', ''), design.strands[0].dna_sequence)
+
+        """
+         012   345   678   901   234   567   890
+        
+        -ACG>                   -TGC>
+        <???---???---???---???---ACG---???---???-
+         098   765   432   109   876   543   210
+        """
+        design.draw_strand(0, 0).to(3).with_sequence('ACG', assign_complement=False)
+        self.assertEqual('ACG'.replace(' ', ''), design.strands[-1].dna_sequence)
+        self.assertEqual('??? ??? GCA ??? ??? ??? ???'.replace(' ', ''), design.strands[0].dna_sequence)
+
+        """
+         012   345   678   901   234   567   890
+              +---------------+
+              |               |
+              |               |
+              |               |
+        -ACG> +TTC>       -AAC+ -TGC>
+        <???---AAG---???---TTG---ACG---???---???-
+         098   765   432   109   876   543   210
+        """
+        sb = design.draw_strand(0, 9).to(12)
+        sb.with_domain_sequence('AAC', assign_complement=True)
+        sb.cross(0, offset=3)
+        sb.to(6)
+        sb.with_domain_sequence('TTC', assign_complement=True)
+        self.assertEqual('AAC TTC'.replace(' ', ''), design.strands[-1].dna_sequence)
+        self.assertEqual('??? ??? GCA GTT ??? GAA ???'.replace(' ', ''), design.strands[0].dna_sequence)
+
+        """
+         012   345   678   901   234   567   890
+              +---------------+
+              |               |
+              |         +-----|-------+   +-+
+              |         |     |       |   | |
+        -ACG> +TTC> -GGA+ -AAC+ -TGC> +TTG+ +GCA>
+        <???---AAG---CCT---TTG---ACG---AAC---CGT-
+         098   765   432   109   876   543   210
+        """
+        design.draw_strand(0, 6).to(9) \
+            .with_domain_sequence('GGA', assign_complement=True) \
+            .cross(0, offset=15) \
+            .to(18) \
+            .with_domain_sequence('TTG', assign_complement=True) \
+            .to(21) \
+            .with_domain_sequence('GCA', assign_complement=True)
+        self.assertEqual('GGA TTG GCA'.replace(' ', ''), design.strands[-1].dna_sequence)
+        self.assertEqual('TGC CAA GCA GTT TCC GAA ???'.replace(' ', ''), design.strands[0].dna_sequence)
+
+
+TEST_OFFSETS_AT_DELETION_INSERTIONS = False
+
+
+class TestSubstrandDNASequenceIn(unittest.TestCase):
+
+    def test_dna_sequence_in__right_then_left(self) -> 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])