From 369ccad0c960472f02f2f73267a996e0c89b941f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20St=C3=A9rin?= Date: Thu, 17 Oct 2024 13:51:11 +0200 Subject: [PATCH 01/11] Debbuging cadnanov2 export of paranemic crossovers when both segments are in the 3' direction --- .gitignore | 1 + scadnano/scadnano.py | 21 ++++++++++----- tests/scadnano_tests.py | 13 +++++++++ ...est_paranemic_crossover_other_direction.sc | 27 +++++++++++++++++++ 4 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 tests_inputs/cadnano_v2_export/test_paranemic_crossover_other_direction.sc diff --git a/.gitignore b/.gitignore index 73dd10a7..b01b0074 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ tests_outputs/ .vscode/ dist/ .mypy_cache/ +test_paranemic.py diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 4ffe57ed..df7f9264 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -6609,10 +6609,15 @@ def _cadnano_v2_place_crossover(helix_from_dct: Dict[str, Any], helix_to_dct: Di helix_to_dct[strand_type][start_to][:2] = [helix_from, start_from] elif forward_from and forward_to: helix_from_dct[strand_type][end_from - 1][2:] = [helix_to, start_to] - helix_to_dct[strand_type][end_to - 1][:2] = [helix_from, start_from] + helix_to_dct[strand_type][end_to-1][:2] = [helix_from, start_from] + if helix_from_dct["row"]%2 != helix_to_dct["row"]%2: + raise ValueError("Paranemic crossovers are only allowed between helices that have the same parity of row number, here helix num "+str(helix_from_dct['num'])+ " and helix num " + str(helix_to_dct['num']) + " have different parity of row number: respectively "+str(helix_from_dct["row"])+" and "+str( helix_to_dct["row"])) + elif not forward_from and not forward_to: helix_from_dct[strand_type][start_from][2:] = [helix_to, end_to - 1] - helix_to_dct[strand_type][start_to][:2] = [helix_from, end_from - 1] + helix_to_dct[strand_type][end_to-1][:2] = [helix_from, start_from] + if helix_from_dct["row"]%2 != helix_to_dct["row"]%2: + raise ValueError("Paranemic crossovers are only allowed between helices that have the same parity of row number, here helix num "+str(helix_from_dct['num'])+ " and helix num " + str(helix_to_dct['num']) + " have different parity of row number: respectively "+str(helix_from_dct["row"])+" and "+str( helix_to_dct["row"])) @staticmethod def _cadnano_v2_color_of_stap(color: Color, domain: Domain) -> List[int]: @@ -6650,6 +6655,9 @@ def _cadnano_v2_place_strand(self, strand: Strand, dct: dict, next_helix = dct['vstrands'][next_helix_id] self._cadnano_v2_place_crossover(which_helix, next_helix, domain, next_domain, strand_type) + + + # if the strand is circular, we need to close the loop if strand.circular: @@ -6784,13 +6792,13 @@ def to_cadnano_v2_serializable(self, name: str = '') -> Dict[str, Any]: if isinstance(domain, Loopout): raise ValueError( 'We cannot handle designs with Loopouts as it is not a cadnano v2 concept') - right_direction: bool + cadnano_expected_direction: bool if hasattr(strand, is_scaffold_key) and strand.is_scaffold: - right_direction = (domain.helix % 2 == int(not domain.forward)) + cadnano_expected_direction = (domain.helix % 2 == int(not domain.forward)) else: - right_direction = not (domain.helix % 2 == int(not domain.forward)) + cadnano_expected_direction = not (domain.helix % 2 == int(not domain.forward)) - if not right_direction: + if not cadnano_expected_direction: raise ValueError('We can only convert designs where even helices have the scaffold' 'going forward and odd helices have the scaffold going backward see ' f'the spec v2.txt Note 4. {domain}') @@ -6803,6 +6811,7 @@ def to_cadnano_v2_serializable(self, name: str = '') -> Dict[str, Any]: for strand in self.strands: self._cadnano_v2_place_strand(strand, dct, helices_ids_reverse) + return dct diff --git a/tests/scadnano_tests.py b/tests/scadnano_tests.py index c3c48ebf..460a1a22 100644 --- a/tests/scadnano_tests.py +++ b/tests/scadnano_tests.py @@ -1629,6 +1629,19 @@ def test_paranemic_crossover(self) -> None: output_design = sc.Design.from_cadnano_v2(json_dict=json.loads(output_json)) self.assertEqual(4, len(output_design.helices)) + def test_paranemic_crossover_other_direction(self) -> None: + design = sc.Design.from_scadnano_file( + os.path.join(self.input_path, f'test_paranemic_crossover_other_direction.{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. diff --git a/tests_inputs/cadnano_v2_export/test_paranemic_crossover_other_direction.sc b/tests_inputs/cadnano_v2_export/test_paranemic_crossover_other_direction.sc new file mode 100644 index 00000000..9369f373 --- /dev/null +++ b/tests_inputs/cadnano_v2_export/test_paranemic_crossover_other_direction.sc @@ -0,0 +1,27 @@ +{ + "version": "0.19.4", + "grid": "square", + "helices": [ + {"grid_position": [19, 14], "idx": 1, "max_offset": 64}, + {"grid_position": [19, 15], "idx": 0, "max_offset": 64}, + {"grid_position": [19, 16], "idx": 3, "max_offset": 64}, + {"grid_position": [19, 17], "idx": 2, "max_offset": 64} + ], + "strands": [ + { + "color": "#0066cc", + "is_scaffold": true, + "domains": [ + {"helix": 3, "forward": false, "start": 8, "end": 24}, + {"helix": 1, "forward": false, "start": 8, "end": 24} + ] + }, + { + "color": "#007200", + "domains": [ + {"helix": 2, "forward": false, "start": 24, "end": 50}, + {"helix": 0, "forward": false, "start": 24, "end": 50} + ] + } + ] +} \ No newline at end of file From 24e4fd4ed51529e7ea5b8e149d81bf3074974b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20St=C3=A9rin?= Date: Thu, 17 Oct 2024 14:01:37 +0200 Subject: [PATCH 02/11] update doc --- doc/index.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/index.rst b/doc/index.rst index 87a50e7d..abb97958 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -31,10 +31,11 @@ Scadnano provides function to convert design to and from cadnano v2: **Important** All ``cadnanov2`` designs can be imported to scadnano. However **not all scadnano designs can be imported -to cadnanov2**, to be importable to ``cadnanov2`` a scadnano design need to comply with the following points: +to cadnanov2**, to be exportable to ``cadnanov2`` a scadnano design need to comply with the following points: * The design cannot feature any :py:class:`Loopout` as it is not a concept that exists in ``cadnanov2``. * Following ``cadnanov2`` conventions, helices with **even** number must have their scaffold going **forward** and helices with **odd** number **backward**. +* If you use paranemic crossovers (i.e. crossovers where the domains before and after the crossover are in the same direction), the helices' row number (i.e. not the helices' indexes but their y coordinate) of the domains must have the same parity e.g. rows 0 and 2, or 1 and 3, but not 0 and 1. Also note that maximum helices offsets can be altered in a ``scadnano`` to ``cadnanov2`` conversion as ``cadnanov2`` needs max offsets to be a multiple of 21 in the hex grid and 32 in the rectangular grid. The conversion algorithm will choose the lowest multiple of 21 or 32 which fits the entire design. From 03d15dbf16f80029839534b5620ad9e5a1c300eb Mon Sep 17 00:00:00 2001 From: David Doty Date: Sat, 26 Oct 2024 13:27:00 -0700 Subject: [PATCH 03/11] Update scadnano.py --- scadnano/scadnano.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index df7f9264..6cfab7d9 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -6611,13 +6611,19 @@ def _cadnano_v2_place_crossover(helix_from_dct: Dict[str, Any], helix_to_dct: Di helix_from_dct[strand_type][end_from - 1][2:] = [helix_to, start_to] helix_to_dct[strand_type][end_to-1][:2] = [helix_from, start_from] if helix_from_dct["row"]%2 != helix_to_dct["row"]%2: - raise ValueError("Paranemic crossovers are only allowed between helices that have the same parity of row number, here helix num "+str(helix_from_dct['num'])+ " and helix num " + str(helix_to_dct['num']) + " have different parity of row number: respectively "+str(helix_from_dct["row"])+" and "+str( helix_to_dct["row"])) + raise ValueError("Paranemic crossovers are only allowed between helices that have the same parity of " + f"row number, here helix num {helix_from_dct['num']} and helix num " + f"{helix_to_dct['num']} have different parity of row number: respectively " + f"{helix_from_dct['row']} and {helix_to_dct["row"]}") elif not forward_from and not forward_to: helix_from_dct[strand_type][start_from][2:] = [helix_to, end_to - 1] helix_to_dct[strand_type][end_to-1][:2] = [helix_from, start_from] if helix_from_dct["row"]%2 != helix_to_dct["row"]%2: - raise ValueError("Paranemic crossovers are only allowed between helices that have the same parity of row number, here helix num "+str(helix_from_dct['num'])+ " and helix num " + str(helix_to_dct['num']) + " have different parity of row number: respectively "+str(helix_from_dct["row"])+" and "+str( helix_to_dct["row"])) + raise ValueError("Paranemic crossovers are only allowed between helices that have the same parity of " + f"row number, here helix num {helix_from_dct['num']} and helix num " + f"{helix_to_dct['num']} have different parity of row number: respectively " + f"{helix_from_dct['row']} and {helix_to_dct['row']}") @staticmethod def _cadnano_v2_color_of_stap(color: Color, domain: Domain) -> List[int]: From b00105b44f9e40e6d3199bc1120cdfea38d68f88 Mon Sep 17 00:00:00 2001 From: David Doty Date: Sat, 26 Oct 2024 13:28:45 -0700 Subject: [PATCH 04/11] formatting --- scadnano/scadnano.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 6cfab7d9..f89e64ea 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -6609,8 +6609,8 @@ def _cadnano_v2_place_crossover(helix_from_dct: Dict[str, Any], helix_to_dct: Di helix_to_dct[strand_type][start_to][:2] = [helix_from, start_from] elif forward_from and forward_to: helix_from_dct[strand_type][end_from - 1][2:] = [helix_to, start_to] - helix_to_dct[strand_type][end_to-1][:2] = [helix_from, start_from] - if helix_from_dct["row"]%2 != helix_to_dct["row"]%2: + helix_to_dct[strand_type][end_to - 1][:2] = [helix_from, start_from] + if helix_from_dct["row"] % 2 != helix_to_dct["row"] % 2: raise ValueError("Paranemic crossovers are only allowed between helices that have the same parity of " f"row number, here helix num {helix_from_dct['num']} and helix num " f"{helix_to_dct['num']} have different parity of row number: respectively " @@ -6618,8 +6618,8 @@ def _cadnano_v2_place_crossover(helix_from_dct: Dict[str, Any], helix_to_dct: Di elif not forward_from and not forward_to: helix_from_dct[strand_type][start_from][2:] = [helix_to, end_to - 1] - helix_to_dct[strand_type][end_to-1][:2] = [helix_from, start_from] - if helix_from_dct["row"]%2 != helix_to_dct["row"]%2: + helix_to_dct[strand_type][end_to - 1][:2] = [helix_from, start_from] + if helix_from_dct["row"] % 2 != helix_to_dct["row"] % 2: raise ValueError("Paranemic crossovers are only allowed between helices that have the same parity of " f"row number, here helix num {helix_from_dct['num']} and helix num " f"{helix_to_dct['num']} have different parity of row number: respectively " @@ -6661,9 +6661,6 @@ def _cadnano_v2_place_strand(self, strand: Strand, dct: dict, next_helix = dct['vstrands'][next_helix_id] self._cadnano_v2_place_crossover(which_helix, next_helix, domain, next_domain, strand_type) - - - # if the strand is circular, we need to close the loop if strand.circular: @@ -6817,7 +6814,6 @@ def to_cadnano_v2_serializable(self, name: str = '') -> Dict[str, Any]: for strand in self.strands: self._cadnano_v2_place_strand(strand, dct, helices_ids_reverse) - return dct From bb00b790e28eeeb647b493c703d03921c5ad6155 Mon Sep 17 00:00:00 2001 From: David Doty Date: Sat, 26 Oct 2024 13:39:38 -0700 Subject: [PATCH 05/11] Update index.rst --- doc/index.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index abb97958..2cde2204 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -25,15 +25,15 @@ Interoperability - cadnano v2 Scadnano provides function to convert design to and from cadnano v2: -* :py:meth:`scadnano.DNADesign.from_cadnano_v2` will create a scadnano DNADesign from a ``cadnanov2`` json file. -* :py:meth:`scadnano.DNADesign.export_cadnano_v2` will produce a ``cadnanov2`` json file from a scadnano design. +* :meth:`scadnano.DNADesign.from_cadnano_v2` will create a scadnano DNADesign from a ``cadnanov2`` json file. +* :meth:`scadnano.DNADesign.export_cadnano_v2` will produce a ``cadnanov2`` json file from a scadnano design. **Important** All ``cadnanov2`` designs can be imported to scadnano. However **not all scadnano designs can be imported to cadnanov2**, to be exportable to ``cadnanov2`` a scadnano design need to comply with the following points: -* The design cannot feature any :py:class:`Loopout` as it is not a concept that exists in ``cadnanov2``. +* The design cannot feature any :class:`Loopout` or :class:`Extension`, since these are not concepts that exist in ``cadnanov2``. * Following ``cadnanov2`` conventions, helices with **even** number must have their scaffold going **forward** and helices with **odd** number **backward**. * If you use paranemic crossovers (i.e. crossovers where the domains before and after the crossover are in the same direction), the helices' row number (i.e. not the helices' indexes but their y coordinate) of the domains must have the same parity e.g. rows 0 and 2, or 1 and 3, but not 0 and 1. From 71278d7b141bbc721167280e0415f0e179d197ac Mon Sep 17 00:00:00 2001 From: David Doty Date: Sat, 26 Oct 2024 13:41:48 -0700 Subject: [PATCH 06/11] Update index.rst --- doc/index.rst | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 2cde2204..7e6a7f11 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -31,14 +31,19 @@ Scadnano provides function to convert design to and from cadnano v2: **Important** All ``cadnanov2`` designs can be imported to scadnano. However **not all scadnano designs can be imported -to cadnanov2**, to be exportable to ``cadnanov2`` a scadnano design need to comply with the following points: - -* The design cannot feature any :class:`Loopout` or :class:`Extension`, since these are not concepts that exist in ``cadnanov2``. -* Following ``cadnanov2`` conventions, helices with **even** number must have their scaffold going **forward** and helices with **odd** number **backward**. -* If you use paranemic crossovers (i.e. crossovers where the domains before and after the crossover are in the same direction), the helices' row number (i.e. not the helices' indexes but their y coordinate) of the domains must have the same parity e.g. rows 0 and 2, or 1 and 3, but not 0 and 1. - -Also note that maximum helices offsets can be altered in a ``scadnano`` to ``cadnanov2`` conversion as ``cadnanov2`` needs max offsets to be a multiple of 21 in the hex grid and 32 in the rectangular grid. -The conversion algorithm will choose the lowest multiple of 21 or 32 which fits the entire design. +to cadnanov2**; to be exportable to ``cadnanov2`` a scadnano design need to comply with the following constraints: + +* The design cannot feature any :class:`Loopout` or :class:`Extension`, since these are not concepts that exist in + ``cadnanov2``. +* Following ``cadnanov2`` conventions, helices with **even** number must have their scaffold going **forward** and + helices with **odd** number **backward**. +* If you use paranemic crossovers (i.e. crossovers where the domains before and after the crossover are in the same + direction), the helices' row number (i.e. not the helices' indexes but their y coordinate) of the domains must have + the same parity, meaning both even or both odd, for example rows 0 and 2, or 1 and 3, but not 0 and 1. + +Also note that maximum helices offsets can be altered in a ``scadnano`` to ``cadnanov2`` conversion as ``cadnanov2`` +needs max offsets to be a multiple of 21 in the hex grid and 32 in the rectangular grid. The conversion algorithm will +choose the lowest multiple of 21 or 32 which fits the entire design. The ``cadnanov2`` json format does not embed sequences hence they will be lost after conversion. From 7d48e95bc3a6b98999ac3593c07c57e0dfe9c6ee Mon Sep 17 00:00:00 2001 From: David Doty Date: Sat, 26 Oct 2024 13:45:25 -0700 Subject: [PATCH 07/11] Update scadnano.py --- scadnano/scadnano.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index f89e64ea..7fd86e13 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -6611,19 +6611,19 @@ def _cadnano_v2_place_crossover(helix_from_dct: Dict[str, Any], helix_to_dct: Di helix_from_dct[strand_type][end_from - 1][2:] = [helix_to, start_to] helix_to_dct[strand_type][end_to - 1][:2] = [helix_from, start_from] if helix_from_dct["row"] % 2 != helix_to_dct["row"] % 2: - raise ValueError("Paranemic crossovers are only allowed between helices that have the same parity of " - f"row number, here helix num {helix_from_dct['num']} and helix num " - f"{helix_to_dct['num']} have different parity of row number: respectively " - f"{helix_from_dct['row']} and {helix_to_dct["row"]}") + raise ValueError(f"""\ +Paranemic crossovers are only allowed between helices that have the same parity of +indices (both even or both odd), but here helix indices {helix_from_dct['num']} and {helix_to_dct['num']} +have different parity: respectively {helix_from_dct['row']} and {helix_to_dct['row']}""") elif not forward_from and not forward_to: helix_from_dct[strand_type][start_from][2:] = [helix_to, end_to - 1] helix_to_dct[strand_type][end_to - 1][:2] = [helix_from, start_from] if helix_from_dct["row"] % 2 != helix_to_dct["row"] % 2: - raise ValueError("Paranemic crossovers are only allowed between helices that have the same parity of " - f"row number, here helix num {helix_from_dct['num']} and helix num " - f"{helix_to_dct['num']} have different parity of row number: respectively " - f"{helix_from_dct['row']} and {helix_to_dct['row']}") + raise ValueError(f"""\ +Paranemic crossovers are only allowed between helices that have the same parity of +indices (both even or both odd), but here helix indices {helix_from_dct['num']} and {helix_to_dct['num']} +have different parity: respectively {helix_from_dct['row']} and {helix_to_dct['row']}""") @staticmethod def _cadnano_v2_color_of_stap(color: Color, domain: Domain) -> List[int]: From 3110228986b91d41c5387047558b177786ffcd19 Mon Sep 17 00:00:00 2001 From: David Doty Date: Sat, 26 Oct 2024 13:50:47 -0700 Subject: [PATCH 08/11] Update scadnano.py --- scadnano/scadnano.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 7fd86e13..b821682d 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -6613,8 +6613,8 @@ def _cadnano_v2_place_crossover(helix_from_dct: Dict[str, Any], helix_to_dct: Di if helix_from_dct["row"] % 2 != helix_to_dct["row"] % 2: raise ValueError(f"""\ Paranemic crossovers are only allowed between helices that have the same parity of -indices (both even or both odd), but here helix indices {helix_from_dct['num']} and {helix_to_dct['num']} -have different parity: respectively {helix_from_dct['row']} and {helix_to_dct['row']}""") +row number, here helix num {helix_from_dct['num']} and helix num {helix_to_dct['num']} +have different parity of row number: respectively {helix_from_dct['row']} and {helix_to_dct['row']}""") elif not forward_from and not forward_to: helix_from_dct[strand_type][start_from][2:] = [helix_to, end_to - 1] @@ -6622,8 +6622,8 @@ def _cadnano_v2_place_crossover(helix_from_dct: Dict[str, Any], helix_to_dct: Di if helix_from_dct["row"] % 2 != helix_to_dct["row"] % 2: raise ValueError(f"""\ Paranemic crossovers are only allowed between helices that have the same parity of -indices (both even or both odd), but here helix indices {helix_from_dct['num']} and {helix_to_dct['num']} -have different parity: respectively {helix_from_dct['row']} and {helix_to_dct['row']}""") +row number, here helix num {helix_from_dct['num']} and helix num {helix_to_dct['num']} +have different parity of row number: respectively {helix_from_dct['row']} and {helix_to_dct['row']}""") @staticmethod def _cadnano_v2_color_of_stap(color: Color, domain: Domain) -> List[int]: From 15ab7b471d640502d2223a039acb9a8564646f46 Mon Sep 17 00:00:00 2001 From: David Doty Date: Sat, 26 Oct 2024 13:59:55 -0700 Subject: [PATCH 09/11] updated explanation of export constraints in doc/index.rst --- doc/index.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 7e6a7f11..4c35f996 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -31,14 +31,18 @@ Scadnano provides function to convert design to and from cadnano v2: **Important** All ``cadnanov2`` designs can be imported to scadnano. However **not all scadnano designs can be imported -to cadnanov2**; to be exportable to ``cadnanov2`` a scadnano design need to comply with the following constraints: +to cadnanov2**; to be exportable to ``cadnanov2`` a scadnano design need to comply with the following constraints. +First, the scadnano field :data:`Helix.idx` is the same as the cadnano notion og helix `num`. We define the +"row number" of a helix to be the order in which it appears in the dict :data:`Design.helices`. A :class:`Helix` +could have a different index from row, for example if one created a design with two helices with indices 0 and 2, +then helix 2 (if appearing last) would have index 1 but row number 2. * The design cannot feature any :class:`Loopout` or :class:`Extension`, since these are not concepts that exist in ``cadnanov2``. * Following ``cadnanov2`` conventions, helices with **even** number must have their scaffold going **forward** and helices with **odd** number **backward**. * If you use paranemic crossovers (i.e. crossovers where the domains before and after the crossover are in the same - direction), the helices' row number (i.e. not the helices' indexes but their y coordinate) of the domains must have + direction), the helices' row number of the two domains being connected by the crossover must have the same parity, meaning both even or both odd, for example rows 0 and 2, or 1 and 3, but not 0 and 1. Also note that maximum helices offsets can be altered in a ``scadnano`` to ``cadnanov2`` conversion as ``cadnanov2`` From bab0cf97ac72bd0485ae9631887ffc0849e2cd14 Mon Sep 17 00:00:00 2001 From: David Doty Date: Sat, 26 Oct 2024 15:32:28 -0700 Subject: [PATCH 10/11] shifted order of cadnano export methods to make it easier to compare with equivalent Dart code --- scadnano/scadnano.py | 241 ++++++++++++++++++++++--------------------- 1 file changed, 121 insertions(+), 120 deletions(-) diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index b821682d..06658820 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -6542,6 +6542,127 @@ def assign_m13_to_scaffold(self, rotation: int = 5587, variant: M13Variant = M13 raise AssertionError('we counted; there is exactly one scaffold') self.assign_dna(scaffold, m13(rotation, variant)) + + def to_cadnano_v2_json(self, name: str = '', whitespace: bool = True) -> str: + """Converts the design to the cadnano v2 format. + + Please see the spec + https://github.com/UC-Davis-molecular-computing/scadnano-python-package/blob/main/misc/cadnano-format-specs/v2.txt + for more info on that format. + + If the cadnano file is intended to be used with CanDo (https://cando-dna-origami.org/), + the optional parameter `whitespace` must be set to False. + + :param name: + Name of the design. + :param whitespace: + Whether to include whitespace in the exported file. Set to False to use this with CanDo + (https://cando-dna-origami.org/), since that tool generates an error if the cadnano file + contains whitespace. + :return: + a string in the cadnano v2 format representing this :any:`Design` + """ + content_serializable = self.to_cadnano_v2_serializable(name) + + encoder = _SuppressableIndentEncoder + content = json.dumps(content_serializable, cls=encoder, indent=2) + if not whitespace: + # remove whitespace + content = ''.join(content.split()) + return content + + def to_cadnano_v2_serializable(self, name: str = '') -> Dict[str, Any]: + """Converts the design to a JSON-serializable Python object (a dict) representing + the cadnano v2 format. Calling json.dumps on this object will result in a string representing + the cadnano c2 format; this is essentially what is done in + :meth:`Design.to_cadnano_v2_json`. + + Please see the spec + https://github.com/UC-Davis-molecular-computing/scadnano-python-package/blob/main/misc/cadnano-format-specs/v2.txt + for more info on that format. + + + :param name: + Name of the design. + :return: + a Python dict representing the cadnano v2 format for this :any:`Design` + """ + dct: Dict[str, Any] = OrderedDict() + if name != '': + dct['name'] = name + + dct['vstrands'] = [] + + '''Check if helix group are used or if only one grid is used''' + if self._has_default_groups(): + design_grid = self.grid + else: + grid_used = {} + assert len(self.groups) > 0 + grid_type = Grid.none + for group_name in self.groups: + grid_used[self.groups[group_name].grid] = True + grid_type = self.groups[group_name].grid + if len(grid_used) > 1: + raise ValueError('Designs using helix groups can be exported to cadnano v2 \ + only if all groups share the same grid type.') + else: + design_grid = grid_type + + '''Figuring out the type of grid. + In cadnano v2, all helices have the same max offset + called `num_bases` and the type of grid is determined as follows: + if num_bases % 32 == 0: then we are on grid square + if num_bases % 21 == 0: then we are on grid honey + ''' + num_bases = 0 + for helix in self.helices.values(): + if helix.max_offset is None: + raise ValueError('must have helix.max_offset set') + num_bases = max(num_bases, helix.max_offset) + + if design_grid == Grid.square: + num_bases = self._get_multiple_of_x_sup_closest_to_y(32, num_bases) + elif design_grid == Grid.honeycomb: + num_bases = self._get_multiple_of_x_sup_closest_to_y(21, num_bases) + else: + raise NotImplementedError('We can export to cadnano v2 `square` and `honeycomb` grids only.') + + '''Figuring out if helices numbers have good parity. + In cadnano v2, only even helices have the scaffold go forward, only odd helices + have the scaffold go backward. + + ''' + for strand in self.strands: + for domain in strand.domains: + if isinstance(domain, Extension): + raise ValueError( + 'We cannot handle designs with Extensions as it is not a cadnano v2 concept') + if isinstance(domain, Loopout): + raise ValueError( + 'We cannot handle designs with Loopouts as it is not a cadnano v2 concept') + cadnano_expected_direction: bool + if hasattr(strand, is_scaffold_key) and strand.is_scaffold: + cadnano_expected_direction = (domain.helix % 2 == int(not domain.forward)) + else: + cadnano_expected_direction = not (domain.helix % 2 == int(not domain.forward)) + + if not cadnano_expected_direction: + raise ValueError('We can only convert designs where even helices have the scaffold' + 'going forward and odd helices have the scaffold going backward see ' + f'the spec v2.txt Note 4. {domain}') + + '''Filling the helices with blank. + ''' + helices_ids_reverse = self._cadnano_v2_fill_blank(dct, num_bases, design_grid) + '''Putting the scaffold in place. + ''' + + for strand in self.strands: + self._cadnano_v2_place_strand(strand, dct, helices_ids_reverse) + + return dct + @staticmethod def _get_multiple_of_x_sup_closest_to_y(x: int, y: int) -> int: return y if y % x == 0 else y + (x - y % x) @@ -6725,126 +6846,6 @@ def _cadnano_v2_fill_blank(self, dct: dict, num_bases: int, design_grid: Grid) - i += 1 return helices_ids_reverse - def to_cadnano_v2_serializable(self, name: str = '') -> Dict[str, Any]: - """Converts the design to a JSON-serializable Python object (a dict) representing - the cadnano v2 format. Calling json.dumps on this object will result in a string representing - the cadnano c2 format; this is essentially what is done in - :meth:`Design.to_cadnano_v2_json`. - - Please see the spec - https://github.com/UC-Davis-molecular-computing/scadnano-python-package/blob/main/misc/cadnano-format-specs/v2.txt - for more info on that format. - - - :param name: - Name of the design. - :return: - a Python dict representing the cadnano v2 format for this :any:`Design` - """ - dct: Dict[str, Any] = OrderedDict() - if name != '': - dct['name'] = name - - dct['vstrands'] = [] - - '''Check if helix group are used or if only one grid is used''' - if self._has_default_groups(): - design_grid = self.grid - else: - grid_used = {} - assert len(self.groups) > 0 - grid_type = Grid.none - for group_name in self.groups: - grid_used[self.groups[group_name].grid] = True - grid_type = self.groups[group_name].grid - if len(grid_used) > 1: - raise ValueError('Designs using helix groups can be exported to cadnano v2 \ - only if all groups share the same grid type.') - else: - design_grid = grid_type - - '''Figuring out the type of grid. - In cadnano v2, all helices have the same max offset - called `num_bases` and the type of grid is determined as follows: - if num_bases % 32 == 0: then we are on grid square - if num_bases % 21 == 0: then we are on grid honey - ''' - num_bases = 0 - for helix in self.helices.values(): - if helix.max_offset is None: - raise ValueError('must have helix.max_offset set') - num_bases = max(num_bases, helix.max_offset) - - if design_grid == Grid.square: - num_bases = self._get_multiple_of_x_sup_closest_to_y(32, num_bases) - elif design_grid == Grid.honeycomb: - num_bases = self._get_multiple_of_x_sup_closest_to_y(21, num_bases) - else: - raise NotImplementedError('We can export to cadnano v2 `square` and `honeycomb` grids only.') - - '''Figuring out if helices numbers have good parity. - In cadnano v2, only even helices have the scaffold go forward, only odd helices - have the scaffold go backward. - - ''' - for strand in self.strands: - for domain in strand.domains: - if isinstance(domain, Extension): - raise ValueError( - 'We cannot handle designs with Extensions as it is not a cadnano v2 concept') - if isinstance(domain, Loopout): - raise ValueError( - 'We cannot handle designs with Loopouts as it is not a cadnano v2 concept') - cadnano_expected_direction: bool - if hasattr(strand, is_scaffold_key) and strand.is_scaffold: - cadnano_expected_direction = (domain.helix % 2 == int(not domain.forward)) - else: - cadnano_expected_direction = not (domain.helix % 2 == int(not domain.forward)) - - if not cadnano_expected_direction: - raise ValueError('We can only convert designs where even helices have the scaffold' - 'going forward and odd helices have the scaffold going backward see ' - f'the spec v2.txt Note 4. {domain}') - - '''Filling the helices with blank. - ''' - helices_ids_reverse = self._cadnano_v2_fill_blank(dct, num_bases, design_grid) - '''Putting the scaffold in place. - ''' - - for strand in self.strands: - self._cadnano_v2_place_strand(strand, dct, helices_ids_reverse) - - return dct - - def to_cadnano_v2_json(self, name: str = '', whitespace: bool = True) -> str: - """Converts the design to the cadnano v2 format. - - Please see the spec - https://github.com/UC-Davis-molecular-computing/scadnano-python-package/blob/main/misc/cadnano-format-specs/v2.txt - for more info on that format. - - If the cadnano file is intended to be used with CanDo (https://cando-dna-origami.org/), - the optional parameter `whitespace` must be set to False. - - :param name: - Name of the design. - :param whitespace: - Whether to include whitespace in the exported file. Set to False to use this with CanDo - (https://cando-dna-origami.org/), since that tool generates an error if the cadnano file - contains whitespace. - :return: - a string in the cadnano v2 format representing this :any:`Design` - """ - content_serializable = self.to_cadnano_v2_serializable(name) - - encoder = _SuppressableIndentEncoder - content = json.dumps(content_serializable, cls=encoder, indent=2) - if not whitespace: - # remove whitespace - content = ''.join(content.split()) - return content - def set_helices_view_order(self, helices_view_order: List[int]) -> None: """ Sets helices_view_order. From ba503f855e2e67a1c9fa75f149b9281dc6422221 Mon Sep 17 00:00:00 2001 From: David Doty Date: Sat, 26 Oct 2024 15:59:12 -0700 Subject: [PATCH 11/11] updated openpyxl, which changed one of the excel files we export --- scadnano/scadnano.py | 2 +- tests/test_excel_export_96.xlsx | Bin 0 -> 11194 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tests/test_excel_export_96.xlsx diff --git a/scadnano/scadnano.py b/scadnano/scadnano.py index 06658820..77eefc49 100644 --- a/scadnano/scadnano.py +++ b/scadnano/scadnano.py @@ -6570,7 +6570,7 @@ def to_cadnano_v2_json(self, name: str = '', whitespace: bool = True) -> str: # remove whitespace content = ''.join(content.split()) return content - + def to_cadnano_v2_serializable(self, name: str = '') -> Dict[str, Any]: """Converts the design to a JSON-serializable Python object (a dict) representing the cadnano v2 format. Calling json.dumps on this object will result in a string representing diff --git a/tests/test_excel_export_96.xlsx b/tests/test_excel_export_96.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..615ff1ea3d2d84f1b1aaf37cf6e27cce88a33e15 GIT binary patch literal 11194 zcma)i1z1$y7cC(oAsvE*bc&>egft9Y(xAXl(jXuyN_Tfi45=W}BHb+^2t!DNNO!+G zfW`a$-}m4n?m1_zz4kt5&boKzD9Rw-z=MN>y9Gz;sii5Q)qwsQxGDyIuz(*UYePjl zYa9Cq&una1oGmTnLgmmJS+T^gaRgrqU1w<)35? z{)FGD(M;vU<=dR&f!!eqSB@spBs!Q!BuPVTra|_&_hk_7s7W*Rhr4ZvPw{0rdXr8_ zf0pqrl6>ZxVOTDx@QF`$h%;xSKZtw29qKdknY`Tk@EKBF)De>6KYBSkWS70MissCp<{~xcOw6fFan6LRhrGc1ydlUDA ze*f0zeBuvM$}*9xh-h_Knj_{ks*j~bd1lMBHqJP9h_dmYh7pj8=XMcma4YQOEY6|LeMH6DgW#UKjdvx+wE#u z5A6vd3v$giCJ*f)wT7hoiv|<^yAy3ckDAsQEs!l87s&7G->4?=m=NE;R-FW$WGbwi zaB#_VaB#Rlb(}38IGBPh!I!_;VD(wm(zYMt!Sh%z`r>S|XNlJO(5t)@*WSz#TAJX! zDo^f%tr24J(kQ~)vrh6MR#)up)kwKdN&GjMmydhJB^Q@?UT&|k7%chvM{hYl9en^D zuC916`CXlz8e5o^D5q$n9u*?1SEgkK*Tm!Afs4~L{yONEj5A)>u+DdF;s;d2KT4q! zPrS`6((gzbJ$2V(Hhug>RY}WQ|6b`lImZEhmud(&6CPJxn6}ElbnDQf+&JR&h z7~FTCT1hfZMh($c7<}$DF&G-q`u>aa?b=NRzox=~)`Bk%D`vEGr-je6mI+KnBxYA! zaPZ6K9}nJx)39+I?jAsl)nhn%5luWwT0J4Gv!SuZ8-RV*+HzyqY=9`1Q^GDK90>!C zySZOh(XB|hJn(yC@ud ze-isax36z5HQ0`-dSXw*eNREsifSaUzJzilT7p_Ut^NL|Ic%OX>D%nMLbp~NV4WzSo~|K1=6!%AK|GUezhK{Qaf}R+=-*TO zJf0b+fe#;dXNohQi@9akIi`N|eKJ2OE>2H0+D&2KIBHs)JYEkuDcmzA$kNPh%mDfsvHN}o+S?A28p7(FZ@gg_+8GB4wJc1Nh0o&BADDQk)ZH4tKJ|hq7F@b z--yj5B4|MLkl3z_z^mLg&-fIoOf|X<&JIrtXTNk75=<`jJvVm;B`H(CyRR@WW8!fAbp^M zo66m!VK7L0Qo>5R`M8j6Y=o1A-ca4Hr@c+JD2m}5Z(9A&Q&Yjz&DEn(YVV>%&HGdY z;WAz&!qk-~L7f|n%_vJo+B7I#s)JzeK%7*OyH?98yP%kn`JyWNN`CFR<)U;4LFT?g ze*MML9yAWo3Okt}<o zh?bvH#`$&(h)@HM2y;)5?)eX1wo4Pw&4)ep94`u%)c_luf%8ktsaG zO*UA3$!0N5M}M|Iau%8mF0(3Pa3Zp#ROW3oi^nx7=$r%>H>i>JscCP0s5KWT#h33p z(|q5ahK?;yfQBPw7)_WB%Gf4Ssa<4}ZdT)|6Xz($iM#W8cA?EyUZoPpu^bbDy{eiw z&jrk8lD#!`A035R+g}rBMoxG2>ta%Jp$nzsQJho5)2&mR4oS@UM~nJ9x}t?1dlR!q zR#A_nWRqXEV8c6zBT&VV+$11mpHp0m48MsB-xJ*-wt?{4D4XO}{1IBs7J3x}e(!rh zUQco6H@@n7u5$RoRWc5_XCPGzf6miGEY1*)ANT zMe~8qsPzejxleZv)f*BhB@~d@&P@1DR^n%$!jh=4IGvz0qNX+py=pAnese27ko4FR${%LV$)oa*pRM2i(AXS0y&6;p|F6AzaNclNMuvkk6NZDkb7>$>)^^YB zO~GIX`v(_)E?fkXtZhA}fhTc}I)jYoR>AZdFAB=vO>h2a(1hC-N3}jLj3eQ2|IJ`* zh{RNWxtM+<`0#`2-PotcADmZ@il*}{{W^r8JuJy`X};5krxpZjsbiM7lP~&Ktvvfg zN34^cd6s!5`z+R4u1#T#Ss8D-rS_HgxNz&TOj=d1#s#f`zrAx>wG={m0JR)E*RDuf z+qEE;^PJpxc6z`W%-tOuw75`w`VGTVaBA=Pc}WxF{)%Na+KLtTGSshR`Q?s>8vZQ2 zb+i7;9v6vjYOun*%!oT_(TSty!;~d|1PRe{=54F?zJ?#=qy zE+O?g7uT(0x@`!`YChCWZwgbvWYCVU%9=beu4g0NwZ_boKmF%D=YI4ayMCvlC=a)c zu9gYsXKp_QDkeibg~|OqJda(@A2)4n>si`CeRgxJ<8Ql0ZHA6cgdslX+Xyp;qI!s* zwCzD$)fD1Pzn^Q#@l{lfYGG`63-CL#|v;|AV6+Gh^>kY6!Vj1 zz*$vh#tfukw!e=bfKjDPJqFPr#beS~kYL0iMTJO}%CIFuyAG1|h`w5zg*emCKj)qn z?8zKcL^6Lb?VOuMJ1r>JB`bDc!%Plcj#}o;=e|KLTqP`)Qo0d|g_|$aa**gU7#F88#Cf3)|Q|Glag3fZW&!UeI%R6EO0Rn-$kJ6%C@U+7WP+Md6n#o!|O12;Kzn5e~n$>dM(!Mte z0}sQ%nF7U<#W1kKijr&tkGgvvHs+seAo3o4tbhn0TOg3(4ik(T90t<~WA?dXuI9&G zQ4(pWj866U)7ZNORA84r{i%+b!cQe;S8t#KF42mtg)piJ7}d)iO%kM830H$KoKxFz zX#vnXNfe}6otoYue?PI`@S(9Np}45a&>;GY&>*AB&^VW&X|9q2z{)VPtE9YN+d_b( z0)V7g`i5Mhm01fRbTbqeq0Iz940pK4a9yL@$tZy0lR#96bQl3t7JQW+QEyl&P`ay9blP}i0of&7&UXpH z)uo{AbQT$SI1_zQ5|&-ZFJ7U6RJWUNuIATwn`+lZgNls(s#1Rq-s*lY$=e&21r+L6 zAlpBIfb2rIVcB(FWoMU64$F?^s%Wz(u#?viKC` z60IHfAK)#s7gyN=B4F9Z0TVy3sS$>nOfId^EP544_4_}85H13#UQH!F50LD)Op(jU z$*n((;D;}k5X57bYCjgcFNK5={unPvwVje;9k+}UpGO!$NmiL6%KNjz&JvzA356H^ z!@Ms!FVlvy8WUB&qoatifqa5-wgi=el)Xf$vF15y!7<@Z0kKaYI5|>Uc!!GZ%rGYA z7#;}(rJu?a)d17bC)KSp${JGmCe=<>STAeL=Ps!rg9MN!?VNkYlkCWSVlyRX6^1U@ z;7--8P^$ZZU`B94_(*D!J-|z;Q?UdEA z*T)u($pyE+I9BgsO|FueoZ64PTc7fEglh7qEtX8Ul%%l`T9a3=#s=+yzgg||SaL7x zJ66Bgx1O+oj+B^jo*sH(Cbi`}JE^y*a@G@iNS-v^w+LnV%>77b-lOFC-f8xP!l#94 z3m38)eYF;z4ab=xx$QCX8UdoZm`D+b~f=!CDl3QwE$yp?l2( zeVFs0&1J-Yn~Cqd_;7FF=lU5p2U@z{*n#5RCh=aX7^$3NMs**DlfmgaeNI}x)*CD; zT@ri~>OSUKvxTiYU*a3VPd-M-@2lkzFIN_@J>oRBNkAI~Q2ELs4&z_r$%*YNyCD`? zgYKMZ1yX$Lfy@{YNIDB3iGgLTyxyXv&rBcr#bRv|o)byuh#I}4Tm~BW#GcqB+`VYX zfHkluLPd{~$!NZ=%Z0RwnlO@gnAH=y7-1$Tt|;=6iEt2s5Nj0;=_rYx@Zn7dEGUza zZ1^M!U?19~iz=m=vK%(R>*vDbVdKQ{;h_XB4BUfti(pIJLPY!1O<5=!fo>(d2!av5 zXtqFV37+q0T%nXWvl4#nQV};$kloTHl*P=G3;foJ!~7Hja4-%;LLmNgxi9FO@TB8y z>hXz_WZ623Bh#Jvy+H{!^@Qf^E!1s37~lj-23%lpn8!UzUX~a1U3klB0rHUy4-*U` zXF+}+Wh0_t-9I!?gr(#R6PRoNNg=k+aIaGn-h9U|5nJ(cvWOe9_=Qh%78gSg+P^ ztmL_#q}@0SJbZ5C6e$}38@>2L;bZG8g=(iaP~j*B3SM(STFaF*l#b$obY>OG-`s=% zHz?~jw=%#TaHUJcPG=dQj=0dpm7okn{##ch3HM)E<#9>|Q*(YYkt?0bIyAq)4|Ysb zypO2?6*MlH4w15X9nmw&(tiaCOrilgAy*O^tl}K#yP~hA1ycD8Ciqt|W@2H$JV2pN zdO}C^bgHz#*wCLz1Xr5;JsRk~8V#E>Usu|{!Vs+`{0`%?V}U%*0n__c!I?)oyW6PK zrY}oWGh6nmh)n`fz}jHHnoab5*&m)&u$A?v0VqW$tc<8Va@i#Nvg~+&dV$Mdy}$(8 zT?UG}TsfV#|HNtgC(hqM0T*_%wS=LO_nXe-^GD0hbRE%2vb>i$TrI2l%j8+E=63cX z`KpUj`E;m$PnG^__F$JlX<)su(s*5zX8vjhkp7{?uQZA5Loe8LqmSX7epNJN%4pMn z17e}VVhOCzyiwKf*(e~Q9FZAdxea8R1kL87fHZSNgr1#Hr=Skz)$8I$vn%+_0dXL$ zDqdZ*^x!nqWn7F(kj!>ovt<-Z%XYddf)8S?GGc!73*6|BiavO#z*t(<4yXIeNq6IO zMQ~ufRQYqRaSjZ3)ub|Q>ebC9V%(6USiqU6W_TC}pq|{wb;ALOg(^h|OcRmX)`aY* z=nzJT63=%gu1ruZL^|%H5`+^*ELp*|=`IKB-A$3BBT`FLqL~`ck{QiaCiX!r9Ow+r zw^^r7Gtr+tRAjtjo1Q@dUG~>1bL2Pn04lG@j8%OK=;*h5-|hxBpWe_ zO86{kZ~v%Z92XP@^h;mrSF7}Rt^6=QQewKkNXA66=q79JP{aDlnr+EC9dv<0$MVeT zB%<9eR6w%mXMI7j007JCe)pa{hbBcSy`JUTZx}{&*dhi218|T}8^Pc~jG-K#d zM<(!Ys~^w*d|2kVFlp$7JX8bEcUn8Z4qoo_DPcYqieDXXJT~ySY)FVMCTC!t^lV|x zB&l4pf629graubjTi0i^ulyNIdbOn!BWJ~Cz8yl-m~Oj!DPy zE+r|lp2A6)nch)m7~QPO;Xt{58*<%5iu|IXQi-LN-zLtBlht+O35$x^7=yM7a>5j{ z+C?7clg+*Y{aV4p1Ebm@_Y$j$G;XJ+*<}Z|lyR>8-EGH%FSR<{LaB-#xA=?vT*`iq z#YuAu_J7@SJb8LDxjAkubO!HE^6B7E?nI*LhrMjlc@Evfw18FILd(uESpQ;9jZWuJP#jdvTaFUUKJG0MTG4hj%UU?QmhthA)8ZttqZi6}P zFE_l$+Y4BDv=m_B_=$~%c&~NUoK6<%LsTA(p8Spa~iN5|BzMvCZkmWBEcO0PbcK6^TH7$ zNYnxE*{6uP@)xPl=YN}?RpgM+3~E^0Z|ACzhE?RE00LU{(-IeksmD-rX@UhX6a1yI zsV5SMr2NbCDr9~mxf0mNSz)F771gHe7qIZlxDm|c&S;tVC;fi;>Hqe*|Cd32$*g#E z>01dNO-)HgoflRMsJL+6|Mb?kc^l5?zf}G4+QKi5MvOLn)UX+}yGjV*(x$K00_vqr z$6b~Y7z;~C=W-q8)nC~3e*pztIAAtiAm`I&nCtOqGRcThHv9jRx$w(?5u@#;DAX%a zI2WQAuWryl7ozgQuS8wuBXT9GSs==7M9yc?FfR*cwh}44I9YP3nzflEI8quaCA19_Y&pm8o?hHX`;RG7K)$5dLpR^H1C7o2`o;P6cF zz6~@>X48!vgaR|$+qp=ENpGWl7$J%`Nh+xtkcxG%kkM$(=^3wp9 z5$dw~+ACLD){>*VB25l@NA5h2IpprlT(XdUWjmcta+XWKD~0S%wI!cGzEu>&IcdQ8+R9_zxt8 zSksF|pHF%UdVOA_C~AmjKnWNiu#*zbaKoCD!+eR@s!B}_(r(8_3fK}3Zz8k1)!Hp3 z6A1+=ANX$+^yg$ItryR5o0{+PGsRw*LJo$92X4>7Y)V&MmJme!^7#{Yd(B? zPf73=`#Oa^tFf?1MyXHM7Cr#UMm*Odf6GlnrN2OL#hWLBg!b$$;hC^r{J>j!E8!w( zMb%q~yFa8ONEwOoy6C9p5rKX^AqMv5;F~Q7ol@`(B6o7t6$6=V72X)2fU~@xQlJk# zrx(wTAAGAJM#Mm=0l*nS5%YRkX-X=EMaU z^-dT>7AkXuixcdQtouC^0$y^k8HMuR4(y4JGJfsDs%TB?+r9btjRD8}A3XCfycYQ#-V_v*7EC(QPZCIw^+MVD2JVceV(!1T_O^p}b!UPcYE<5i z^hnD+LHqaa&}}<&TnRX(u!jN%cl+|A6nlr47GQhWD`(;fG!V*)b&5ZYnHv?)Wh=d< zDEc)0K3X-VyP5$n_R>t}R^-mgBPw3Uk^)V7_X4?%ZjzL$yC#Fj1vq^s6#Uhu;3G@N zz1rjC#nVk@&UK+t`8$ubRyg^`>Ha%jz|&<%aN|K!aVDUXl7ek4}GN^ljD=|dc$OOmdO#{(a~t+d^Q zc7GU3x`h&A;DC$2bDmSNbeuNvQNC8|9Q?{$_KPRRa1b4x)=QKlNKKfK4@SVac-lMk zIKw3OwiJ~TG{-wxTLZyknh!%**Ub_4Q^}v^t)Dab;KZyH2}M`vJmgN+d*2EHYLnX&g= zwJ-4XNvVPMi6b1Q!j;sP%~Wi|h2^})NQ2Wt-#m(0z(bOc z=?rmsW0sXqf5VBbCKD{}kkkA~fc(dOhQNFfl|O`o9u>8rB0?e;ofR3CA#j7#ZCjwz zdMeNDET`i5_P=W{F0h)U9$0g@!1_Z2{_5L-E$kn#z<#GhR#`Q%qCfU*`I5?t{Ds1w z1H4Ia;}HHy2Ie;$q(JeMx}q_NZ8#dyO96hIaWb>Vt2|ldL3wvhv6RiUzE;9ZmD7Zo z^+t#k4l$E)`3*c4gBpr{&|(&R%tB;bQDa1qW1pq&uQQSY5r95W5Vn3*#3ySjVkRZS zfw9pu;K{zfjm{-L52A|ECUzd2KXd#p-JedWL69ikF_=LL@g!~A3m;rAGNdeZ(Ox>V zHC06o9XF&2wlyd{<9gkJ`O^dL-$c8%p7v|N!@;!yZiRXIB7=QiX<%*r9QHD+iRuS6 zJ;oCEr1^;)&l!RwGe{Vug?0al+Z1h-@zR?7w~5lRBBzfOcd!B6fV znl~8W7OZz+_UM3OZ0WA)D1IKVk-pi{!aVJTueI0|!m7_ci+y0HUzSRy`0@kO@^rs4 z^zEpVp>vRPsl>)J9#`DOy$27K)t2jz-51nSBznh2LFfYh8TF^Bu8;IVzumx)E`m6Z)@S?9 z-ajZLBHS%a;%0$>t|FDK!0!TmiI z7mKeYiWdVoO(tGsa0rJ^?S+8Nm{>|r_@Ubfdy}&7UL@4;?X6gPQ$@y=u=#y`Ho;_D z#qmcny6e)x#vfEWLr2%)rJPLh-X}}mwI(JEp1C86rng*Rj{R}h1$jD(BFs-AJC`o3 z4oi1%hgd;=3SH|A!M7Ss{Rft2pq39oRWvKccKr7j7EdCW#a}Rhb6M`9$e@7(6w5y= zrrw(GLcEm zba~oJTh!Xh0c_=g*p4pnODQQub6(;qj?YOW-g=| zSUXsUL9L=5XBsf#RCAhJcnY| z(nmyL8*tj2G7O$sZ`D*9hln(IzfU8i&ABpC!;Aw#ACqacsUr4nDhA6WFZZj*u)N)A z8MX_k#}bawKn&+Hz)A^;3ZO%8+Baseiz7m`Ti`o-^;5&U%T`k6(+}nkSYiXSlKj}3 zE?b9h?W?`F;w;G4{letdkvlyS1ZRuL(smyFG_<90AJ>y6cRK&pR=wY&uYYQV|HRB` zuItQQ*BwDo1|9(q?th<31QylB*DIjf|MwZi>*(ue1TV30aIfG${Ehx~V(>cr`XRbM z@LnMNe;ld1PH_DY#V>+#FU(sP1pj%Y;yTOqkAVKL!~r`UAmcjA#Ro#yS+2iF^@oL@ z@IP5D-m$vQa{Yne4+}oof3jRW9$aU+en<3+xT9RI!yJCy