diff --git a/.github/workflows/run_unit_tests.yml b/.github/workflows/run_unit_tests.yml index 3e86097..8a4c017 100644 --- a/.github/workflows/run_unit_tests.yml +++ b/.github/workflows/run_unit_tests.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ 3.7, 3.8, 3.9, "3.10", "3.11" ] + python-version: [ 3.7, 3.8, 3.9, "3.10", "3.11", "3.12" ] steps: - uses: actions/checkout@v2 @@ -21,8 +21,8 @@ jobs: with: activate-conda: true - name: Install openpyxl,tabulate with conda - run: conda install openpyxl=3.0.10 tabulate=0.8.10 + run: conda install openpyxl=3.1 tabulate=0.9 - name: Install docutils with conda - run: conda install docutils=0.16 + run: conda install docutils=0.18 - name: Test with unittest run: python -m unittest -v tests/scadnano_tests.py diff --git a/environment.yml b/environment.yml index beb0796..dae5b56 100644 --- a/environment.yml +++ b/environment.yml @@ -4,7 +4,6 @@ channels: - defaults dependencies: - python=3.7 - - xlwt=1.3.0=py37_0 - pip - pip: - sphinx diff --git a/examples/1_staple_1_helix_origami_roll.py b/examples/1_staple_1_helix_origami_roll.py index ff3f741..e8a8e59 100644 --- a/examples/1_staple_1_helix_origami_roll.py +++ b/examples/1_staple_1_helix_origami_roll.py @@ -1,5 +1,3 @@ -import math - import scadnano as sc def create_design() -> sc.Design: diff --git a/examples/2_staple_2_helix_helixgroup_geometry.py b/examples/2_staple_2_helix_helixgroup_geometry.py new file mode 100644 index 0000000..b89166f --- /dev/null +++ b/examples/2_staple_2_helix_helixgroup_geometry.py @@ -0,0 +1,24 @@ +import scadnano as sc + + +def create_design() -> sc.Design: + group0 = sc.HelixGroup(grid=sc.square) + group1 = sc.HelixGroup(grid=sc.square, geometry=sc.Geometry(bases_per_turn=18), + position=sc.Position3D(0, 3, 0)) + groups = {"group 0": group0, "group 1": group1} + helices = [sc.Helix(idx=idx, max_offset=40, group=group) for idx, group in + [(0, "group 0"), (1, "group 1")]] + design = sc.Design(helices=helices, groups=groups, strands=[]) + design.draw_strand(0, 0).move(40) + design.draw_strand(0, 40).move(-40) + design.draw_strand(1, 0).move(40) + design.draw_strand(1, 40).move(-40) + + return design + + +if __name__ == '__main__': + d = create_design() + d.write_scadnano_file(directory='output_designs') + d.from_scadnano_file('output_designs/2_staple_2_helix_helixgroup_geometry.sc') + print(f'design: {d.to_json()}') diff --git a/examples/output_designs/2_staple_2_helix_helixgroup_geometry.sc b/examples/output_designs/2_staple_2_helix_helixgroup_geometry.sc new file mode 100644 index 0000000..6625a19 --- /dev/null +++ b/examples/output_designs/2_staple_2_helix_helixgroup_geometry.sc @@ -0,0 +1,46 @@ +{ + "version": "0.19.4", + "groups": { + "group 0": { + "position": {"x": 0, "y": 0, "z": 0}, + "grid": "square" + }, + "group 1": { + "position": {"x": 0, "y": 3, "z": 0}, + "grid": "square", + "geometry": { + "bases_per_turn": 18 + } + } + }, + "helices": [ + {"group": "group 0", "grid_position": [0, 0]}, + {"group": "group 1", "grid_position": [0, 0]} + ], + "strands": [ + { + "color": "#f74308", + "domains": [ + {"helix": 0, "forward": true, "start": 0, "end": 40} + ] + }, + { + "color": "#57bb00", + "domains": [ + {"helix": 0, "forward": false, "start": 0, "end": 40} + ] + }, + { + "color": "#888888", + "domains": [ + {"helix": 1, "forward": true, "start": 0, "end": 40} + ] + }, + { + "color": "#32b86c", + "domains": [ + {"helix": 1, "forward": false, "start": 0, "end": 40} + ] + } + ] +} \ No newline at end of file diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 548ee8a..e1edc81 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -54,7 +54,7 @@ # needed to use forward annotations: https://docs.python.org/3/whatsnew/3.7.html#whatsnew37-pep563 from __future__ import annotations -__version__ = "0.19.4" # version line; WARNING: do not remove or change this line or comment +__version__ = "0.19.5" # version line; WARNING: do not remove or change this line or comment import collections import dataclasses @@ -1315,6 +1315,14 @@ class HelixGroup(_JSONSerializable): grid: Grid = Grid.none """:any:`Grid` of this :any:`HelixGroup` used to interpret the field :data:`Helix.grid_position`.""" + geometry: Optional[Geometry] = None + """ + Optional custom :any:`Geometry` to specify for this :any:`HelixGroup`. If specified then + it is assumed to override the field :data:`Design.geometry`. This will affect, for instance, + where nucleotides and phosphate groups are placed when exporting to oxDNA or oxView via + :meth:`Design.to_oxview_format` or :meth:`Design.to_oxdna_format`. + """ + 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 @@ -1341,6 +1349,9 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> D if self.helices_view_order != default_helices_view_order: dct[helices_view_order_key] = NoIndent(self.helices_view_order) + if self.geometry is not None: + dct[geometry_key] = self.geometry.to_json_serializable(suppress_indent) + return dct def _assign_default_helices_view_order(self, helices_in_group: Dict[int, 'Helix']) -> None: @@ -1372,12 +1383,19 @@ def from_json(json_map: dict, **kwargs: Any) -> HelixGroup: roll = json_map.get(roll_key, default_roll) yaw = json_map.get(yaw_key, default_yaw) - return HelixGroup(position=position, - pitch=pitch, - yaw=yaw, - roll=roll, - helices_view_order=helices_view_order, - grid=grid) + geometry = None + if geometry_key in json_map: + geometry = Geometry.from_json(json_map[geometry_key]) + + return HelixGroup( + position=position, + pitch=pitch, + yaw=yaw, + roll=roll, + helices_view_order=helices_view_order, + grid=grid, + geometry=geometry, + ) def helices_view_order_inverse(self, idx: int) -> int: """ diff --git a/tests/scadnano_tests.py b/tests/scadnano_tests.py index c3c48eb..181edf8 100644 --- a/tests/scadnano_tests.py +++ b/tests/scadnano_tests.py @@ -1282,7 +1282,7 @@ def test_write_idt_plate_excel_file(self) -> None: # 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()}.xlsx' + filename = f'tests/test_excel_export_{plate_type.num_wells_per_plate()}.xlsx' 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) @@ -1307,7 +1307,10 @@ def test_write_idt_plate_excel_file(self) -> None: self.assertEqual(expected_wells + 1, sheet.max_row) - os.remove(filename) + try: + os.remove(filename) + except PermissionError as e: + print(f'could not remove file "{filename}" due to permission error') def test_export_dna_sequences_extension_5p(self) -> None: design = sc.Design(helices=[sc.Helix(max_offset=100)]) diff --git a/tests/test_excel_export_384.xlsx b/tests/test_excel_export_384.xlsx new file mode 100644 index 0000000..c1f3d03 Binary files /dev/null and b/tests/test_excel_export_384.xlsx differ diff --git a/tests/test_excel_export_96.xls b/tests/test_excel_export_96.xls deleted file mode 100644 index bbdb3dd..0000000 Binary files a/tests/test_excel_export_96.xls and /dev/null differ diff --git a/tests/test_excel_export_96.xlsx b/tests/test_excel_export_96.xlsx new file mode 100644 index 0000000..d506e23 Binary files /dev/null and b/tests/test_excel_export_96.xlsx differ