@@ -858,7 +858,10 @@ def m13(rotation: int = 5587, variant: M13Variant = M13Variant.p7249) -> str:
858858scaffold_key = 'scaffold'
859859helices_view_order_key = 'helices_view_order'
860860is_origami_key = 'is_origami'
861- design_modifications_key = 'modifications_in_design'
861+ design_modifications_key = 'modifications_in_design' # legacy key for when we stored all mods in one dict
862+ design_modifications_5p_key = 'modifications_5p_in_design'
863+ design_modifications_3p_key = 'modifications_3p_in_design'
864+ design_modifications_int_key = 'modifications_int_in_design'
862865geometry_key = 'geometry'
863866groups_key = 'groups'
864867
@@ -966,12 +969,22 @@ class ModificationType(enum.Enum):
966969 five_prime = "5'"
967970 """5' modification type"""
968971
969- three_prime = "5 '"
972+ three_prime = "3 '"
970973 """3' modification type"""
971974
972975 internal = "internal"
973976 """internal modification type"""
974977
978+ def key (self ) -> str :
979+ if self == ModificationType .five_prime :
980+ return design_modifications_5p_key
981+ elif self == ModificationType .three_prime :
982+ return design_modifications_3p_key
983+ elif self == ModificationType .internal :
984+ return design_modifications_int_key
985+ else :
986+ raise AssertionError (f'unknown ModificationType { self } ' )
987+
975988
976989@dataclass (frozen = True , eq = True )
977990class Modification (_JSONSerializable , ABC ):
@@ -3962,16 +3975,20 @@ def default_export_name(self, unique_names: bool = False) -> str:
39623975 name = f'{ start_helix } [{ start_offset } ]{ forward_str } { end_helix } [{ end_offset } ]'
39633976 return f'SCAF{ name } ' if self .is_scaffold else f'ST{ name } '
39643977
3965- def set_modification_5p (self , mod : Modification5Prime = None ) -> None :
3966- """Sets 5' modification to be `mod`. `mod` cannot be non-None if :any:`Strand.circular` is True ."""
3967- if self .circular and mod is not None :
3978+ def set_modification_5p (self , mod : Modification5Prime ) -> None :
3979+ """Sets 5' modification to be `mod`. :any:`Strand.circular` must be False ."""
3980+ if self .circular :
39683981 raise StrandError (self , "cannot have a 5' modification on a circular strand" )
3982+ if not isinstance (mod , Modification5Prime ):
3983+ raise TypeError (f'mod must be a Modification5Prime but it is type { type (mod )} : { mod } ' )
39693984 self .modification_5p = mod
39703985
3971- def set_modification_3p (self , mod : Modification3Prime = None ) -> None :
3972- """Sets 3' modification to be `mod`. `mod` cannot be non-None if :any:`Strand.circular` is True ."""
3986+ def set_modification_3p (self , mod : Modification3Prime ) -> None :
3987+ """Sets 3' modification to be `mod`. :any:`Strand.circular` must be False ."""
39733988 if self .circular and mod is not None :
39743989 raise StrandError (self , "cannot have a 3' modification on a circular strand" )
3990+ if not isinstance (mod , Modification3Prime ):
3991+ raise TypeError (f'mod must be a Modification3Prime but it is type { type (mod )} : { mod } ' )
39753992 self .modification_3p = mod
39763993
39773994 def remove_modification_5p (self ) -> None :
@@ -3999,6 +4016,8 @@ def set_modification_internal(self, idx: int, mod: ModificationInternal,
39994016 elif warn_on_no_dna :
40004017 print ('WARNING: no DNA sequence has been assigned, so certain error checks on the internal '
40014018 'modification were not done. To be safe, first assign DNA, then add the modifications.' )
4019+ if not isinstance (mod , ModificationInternal ):
4020+ raise TypeError (f'mod must be a ModificationInternal but it is type { type (mod )} : { mod } ' )
40024021 self .modifications_int [idx ] = mod
40034022
40044023 def remove_modification_internal (self , idx : int ) -> None :
@@ -5763,10 +5782,27 @@ def from_scadnano_json_map(
57635782 strand = Strand .from_json (strand_json )
57645783 strands .append (strand )
57655784
5766- # modifications in whole design
5785+ mods_5p : Dict [str , Modification5Prime ] = {}
5786+ mods_3p : Dict [str , Modification3Prime ] = {}
5787+ mods_int : Dict [str , ModificationInternal ] = {}
5788+ for all_mods_key , mods in zip ([design_modifications_5p_key ,
5789+ design_modifications_3p_key ,
5790+ design_modifications_int_key ], [mods_5p , mods_3p , mods_int ]):
5791+ if all_mods_key in json_map :
5792+ all_mods_json = json_map [all_mods_key ]
5793+ for mod_key , mod_json in all_mods_json .items ():
5794+ mod = Modification .from_json (mod_json )
5795+ if mod_key != mod .vendor_code :
5796+ print (f'WARNING: key { mod_key } does not match vendor_code field { mod .vendor_code } '
5797+ f'for modification { mod } \n '
5798+ f'replacing with key = { mod .vendor_code } ' )
5799+ mod = dataclasses .replace (mod , vendor_code = mod_key )
5800+ mods [mod_key ] = mod
5801+
5802+ # legacy code; now we stored modifications in 3 separate dicts depending on 5', 3', internal
5803+ all_mods : Dict [str , Modification ] = {}
57675804 if design_modifications_key in json_map :
57685805 all_mods_json = json_map [design_modifications_key ]
5769- all_mods = {}
57705806 for mod_key , mod_json in all_mods_json .items ():
57715807 mod = Modification .from_json (mod_json )
57725808 if mod_key != mod .vendor_code :
@@ -5775,7 +5811,8 @@ def from_scadnano_json_map(
57755811 f'replacing with key = { mod .vendor_code } ' )
57765812 mod = dataclasses .replace (mod , vendor_code = mod_key )
57775813 all_mods [mod_key ] = mod
5778- Design .assign_modifications_to_strands (strands , strand_jsons , all_mods )
5814+
5815+ Design .assign_modifications_to_strands (strands , strand_jsons , mods_5p , mods_3p , mods_int , all_mods )
57795816
57805817 geometry = None
57815818 if geometry_key in json_map :
@@ -5831,19 +5868,25 @@ def to_json_serializable(self, suppress_indent: bool = True, **kwargs: Any) -> D
58315868 self .helices_view_order ) if suppress_indent else self .helices_view_order
58325869
58335870 # modifications
5834- mods = self .modifications ()
5835- if len (mods ) > 0 :
5836- mods_dict = {}
5837- for mod in mods :
5838- if mod .vendor_code not in mods_dict :
5839- mods_dict [mod .vendor_code ] = mod .to_json_serializable (suppress_indent )
5840- else :
5841- if mod != mods_dict [mod .vendor_code ]:
5842- raise IllegalDesignError (f"Modifications must have unique vendor codes, but I found"
5843- f"two different Modifications that share vendor code "
5844- f"{ mod .vendor_code } :\n { mod } \n and\n "
5845- f"{ mods_dict [mod .vendor_code ]} " )
5846- dct [design_modifications_key ] = mods_dict
5871+ for mod_type in [ModificationType .five_prime ,
5872+ ModificationType .three_prime ,
5873+ ModificationType .internal ]:
5874+ mods = self .modifications (mod_type )
5875+ mod_key = mod_type .key ()
5876+ if len (mods ) > 0 :
5877+ mods_dict = {}
5878+ for mod in mods :
5879+ if mod .vendor_code not in mods_dict :
5880+ mods_dict [mod .vendor_code ] = mod .to_json_serializable (suppress_indent )
5881+ else :
5882+ if mod != mods_dict [mod .vendor_code ]:
5883+ raise IllegalDesignError (
5884+ f"Modifications of type { mod_type } must have unique vendor codes, "
5885+ f"but I foundtwo different Modifications of that type "
5886+ f"that share vendor code "
5887+ f"{ mod .vendor_code } :\n { mod } \n and\n "
5888+ f"{ mods_dict [mod .vendor_code ]} " )
5889+ dct [mod_key ] = mods_dict
58475890
58485891 dct [strands_key ] = [strand .to_json_serializable (suppress_indent ) for strand in self .strands ]
58495892
@@ -5940,19 +5983,34 @@ def base_pairs(self, allow_mismatches: bool = False) -> Dict[int, List[int]]:
59405983
59415984 @staticmethod
59425985 def assign_modifications_to_strands (strands : List [Strand ], strand_jsons : List [dict ],
5986+ mods_5p : Dict [str , Modification5Prime ],
5987+ mods_3p : Dict [str , Modification3Prime ],
5988+ mods_int : Dict [str , ModificationInternal ],
59435989 all_mods : Dict [str , Modification ]) -> None :
5990+ if len (all_mods ) > 0 : # legacy code for when modifications were stored in a single dict
5991+ assert len (mods_5p ) == 0 and len (mods_3p ) == 0 and len (mods_int ) == 0
5992+ legacy = True
5993+ elif len (mods_5p ) > 0 or len (mods_3p ) > 0 or len (mods_int ) > 0 :
5994+ assert len (all_mods ) == 0
5995+ legacy = False
5996+ else : # no modifications
5997+ return
5998+
59445999 for strand , strand_json in zip (strands , strand_jsons ):
59456000 if modification_5p_key in strand_json :
5946- mod_name = strand_json [modification_5p_key ]
5947- strand .modification_5p = cast (Modification5Prime , all_mods [mod_name ])
6001+ mod_code = strand_json [modification_5p_key ]
6002+ strand .modification_5p = cast (Modification5Prime , all_mods [mod_code ]) \
6003+ if legacy else mods_5p [mod_code ]
59486004 if modification_3p_key in strand_json :
5949- mod_name = strand_json [modification_3p_key ]
5950- strand .modification_3p = cast (Modification3Prime , all_mods [mod_name ])
6005+ mod_code = strand_json [modification_3p_key ]
6006+ strand .modification_3p = cast (Modification3Prime , all_mods [mod_code ]) \
6007+ if legacy else mods_3p [mod_code ]
59516008 if modifications_int_key in strand_json :
59526009 mod_names_by_offset = strand_json [modifications_int_key ]
5953- for offset_str , mod_name in mod_names_by_offset .items ():
6010+ for offset_str , mod_code in mod_names_by_offset .items ():
59546011 offset = int (offset_str )
5955- strand .modifications_int [offset ] = cast (ModificationInternal , all_mods [mod_name ])
6012+ strand .modifications_int [offset ] = cast (ModificationInternal , all_mods [mod_code ]) \
6013+ if legacy else mods_int [mod_code ]
59566014
59576015 @staticmethod
59586016 def _cadnano_v2_import_find_5_end (vstrands : VStrands , strand_type : str , helix_num : int , base_id : int ,
@@ -6079,7 +6137,7 @@ def _cadnano_v2_import_explore_domains(vstrands: VStrands, seen: Dict[Tuple[int,
60796137 @staticmethod
60806138 def _cadnano_v2_import_circular_strands_merge_first_last_domains (domains : List [Domain ]) -> None :
60816139 """ When we create domains for circular strands in the cadnano import routine, we may end up
6082- with a fake crossover if first and last domain are on same helix, we have to merge them
6140+ with a fake crossover if first and last domain are on same helix, we have to merge them
60836141 if it is the case.
60846142 """
60856143 if domains [0 ].helix != domains [- 1 ].helix :
@@ -6210,9 +6268,9 @@ def from_cadnano_v2(directory: str = '', filename: Optional[str] = None,
62106268 # TS: Dave, I have thorougly checked the code of Design constructor and the order of the helices
62116269 # IS lost even if the helices were give as a list.
62126270 # Indeed, you very early call `_normalize_helices_as_dict` in the constructor the order is lost.
6213- # Later in the code, if no view order was given the code will choose the identity
6271+ # Later in the code, if no view order was given the code will choose the identity
62146272 # in function `_check_helices_view_order_and_return`.
6215- # Conclusion: do not assume that your constructor code deals with the ordering, even if
6273+ # Conclusion: do not assume that your constructor code deals with the ordering, even if
62166274 # input helices is a list. I am un commenting the below:
62176275 design .set_helices_view_order ([num for num in helices ])
62186276
@@ -7641,7 +7699,7 @@ def _write_plates_default(self, directory: str, filename: Optional[str], strands
76417699
76427700 # IDT charges extra for a plate with < 24 strands for 96-well plate
76437701 # or < 96 strands for 384-well plate.
7644- # So if we would have fewer than that many on the last plate,
7702+ # So if we would have fewer than that many on the last plate,
76457703 # shift some from the penultimate plate.
76467704 if not on_final_plate and \
76477705 final_plate_less_than_min_required and \
@@ -7670,7 +7728,7 @@ def to_oxview_format(self, warn_duplicate_strand_names: bool = True,
76707728 have duplicate names. (default: True)
76717729 :param use_strand_colors:
76727730 if True (default), sets the color of each nucleotide in a strand in oxView to the color
7673- of the strand.
7731+ of the strand.
76747732 """
76757733 import datetime
76767734 self ._check_legal_design (warn_duplicate_strand_names )
@@ -8184,7 +8242,8 @@ def ligate(self, helix: int, offset: int, forward: bool) -> None:
81848242 strand_3p .domains .append (dom_new )
81858243 strand_3p .domains .extend (strand_5p .domains [1 :])
81868244 strand_3p .is_scaffold = strand_left .is_scaffold or strand_right .is_scaffold
8187- strand_3p .set_modification_3p (strand_5p .modification_3p )
8245+ if strand_5p .modification_3p is not None :
8246+ strand_3p .set_modification_3p (strand_5p .modification_3p )
81888247 for idx , mod in strand_5p .modifications_int .items ():
81898248 new_idx = idx + strand_3p .dna_length ()
81908249 strand_3p .set_modification_internal (new_idx , mod )
0 commit comments