synbio-buildcompiler 0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- buildcompiler/__init__.py +1 -0
- buildcompiler/abstract_translator.py +346 -0
- buildcompiler/buildcompiler.py +49 -0
- buildcompiler/constants.py +15 -0
- buildcompiler/robotutils.py +151 -0
- buildcompiler/sbol2build.py +1110 -0
- synbio_buildcompiler-0.0b1.dist-info/METADATA +79 -0
- synbio_buildcompiler-0.0b1.dist-info/RECORD +11 -0
- synbio_buildcompiler-0.0b1.dist-info/WHEEL +5 -0
- synbio_buildcompiler-0.0b1.dist-info/licenses/LICENSE +21 -0
- synbio_buildcompiler-0.0b1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .sbol2build import * # noqa: F403
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import sbol2
|
|
2
|
+
import itertools
|
|
3
|
+
from typing import Dict, List, Union
|
|
4
|
+
from .constants import FUSION_SITES
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MocloPlasmid:
|
|
8
|
+
def __init__(
|
|
9
|
+
self, name: str, definition: sbol2.ComponentDefinition, doc: sbol2.document
|
|
10
|
+
):
|
|
11
|
+
self.definition = definition
|
|
12
|
+
self.fusion_sites = self.match_fusion_sites(doc)
|
|
13
|
+
self.name = name + "".join(f"_{s}" for s in self.fusion_sites)
|
|
14
|
+
|
|
15
|
+
def match_fusion_sites(self, doc: sbol2.document) -> List[str]:
|
|
16
|
+
fusion_site_definitions = extract_fusion_sites(self.definition, doc)
|
|
17
|
+
fusion_sites = []
|
|
18
|
+
for site in fusion_site_definitions:
|
|
19
|
+
sequence_obj = doc.getSequence(site.sequences[0])
|
|
20
|
+
sequence = sequence_obj.elements
|
|
21
|
+
|
|
22
|
+
for key, seq in FUSION_SITES.items():
|
|
23
|
+
if seq == sequence.upper():
|
|
24
|
+
fusion_sites.append(key)
|
|
25
|
+
|
|
26
|
+
fusion_sites.sort()
|
|
27
|
+
return fusion_sites
|
|
28
|
+
|
|
29
|
+
def __repr__(self) -> str:
|
|
30
|
+
return (
|
|
31
|
+
f"MocloPlasmid:\n"
|
|
32
|
+
f" Name: {self.name}\n"
|
|
33
|
+
f" Definition: {self.definition.identity}\n"
|
|
34
|
+
f" Fusion Sites: {self.fusion_sites or 'Not found'}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def __eq__(self, other):
|
|
38
|
+
if not isinstance(other, MocloPlasmid):
|
|
39
|
+
return False
|
|
40
|
+
return self.definition == other.definition
|
|
41
|
+
|
|
42
|
+
def __hash__(self):
|
|
43
|
+
return hash(self.definition)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def extract_fusion_sites(
|
|
47
|
+
plasmid: sbol2.ComponentDefinition, doc: sbol2.Document
|
|
48
|
+
) -> List[sbol2.ComponentDefinition]:
|
|
49
|
+
"""
|
|
50
|
+
Returns all fusion site component definitions from a plasmid.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
plasmid: :class:`sbol2.ComponentDefinition` representing the plasmid.
|
|
54
|
+
doc: :class:`sbol2.Document` containing component definitions.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
A list of fusion site component definitions.
|
|
58
|
+
"""
|
|
59
|
+
fusion_sites = []
|
|
60
|
+
for component in plasmid.components:
|
|
61
|
+
definition = doc.getComponentDefinition(component.definition)
|
|
62
|
+
if "http://identifiers.org/so/SO:0001953" in definition.roles:
|
|
63
|
+
fusion_sites.append(definition)
|
|
64
|
+
|
|
65
|
+
return fusion_sites
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def extract_design_parts(
|
|
69
|
+
design: sbol2.ComponentDefinition, doc: sbol2.Document
|
|
70
|
+
) -> List[sbol2.ComponentDefinition]:
|
|
71
|
+
"""
|
|
72
|
+
Returns definitions of parts in a design in sequential order.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
design: :class:`sbol2.ComponentDefinition` to extract parts from.
|
|
76
|
+
doc: :class:`sbol2.Document` containing all component definitions.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
A list of component definitions in sequential order.
|
|
80
|
+
"""
|
|
81
|
+
component_list = [c for c in design.getInSequentialOrder()]
|
|
82
|
+
return [
|
|
83
|
+
doc.getComponentDefinition(component.definition) for component in component_list
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def copy_sequences(component_definition, target_doc, collection_doc):
|
|
88
|
+
"""Copy all sequences referenced by a ComponentDefinition into target_doc."""
|
|
89
|
+
subdefinitions = extract_design_parts(component_definition, collection_doc)
|
|
90
|
+
|
|
91
|
+
for seq_uri in component_definition.sequences:
|
|
92
|
+
seq_obj = component_definition.doc.find(seq_uri)
|
|
93
|
+
if seq_obj is not None:
|
|
94
|
+
seq_obj.copy(target_doc)
|
|
95
|
+
|
|
96
|
+
for subdefinition in subdefinitions:
|
|
97
|
+
print(subdefinition.displayId)
|
|
98
|
+
subdefinition.copy(target_doc)
|
|
99
|
+
for seq_uri in subdefinition.sequences:
|
|
100
|
+
seq_obj = component_definition.doc.find(seq_uri)
|
|
101
|
+
if seq_obj is not None:
|
|
102
|
+
seq_obj.copy(target_doc)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def extract_combinatorial_design_parts(
|
|
106
|
+
design: sbol2.ComponentDefinition, doc: sbol2.Document, plasmid_doc
|
|
107
|
+
) -> Dict[str, List[sbol2.ComponentDefinition]]:
|
|
108
|
+
"""
|
|
109
|
+
Extracts and returns a mapping of component definitions from a combinatorial design, in order.
|
|
110
|
+
Variants of combinatinatorial components are entered in a list corresponding to the URI of the component in the abstract design.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
design:
|
|
114
|
+
The :class:`sbol2.ComponentDefinition` representing the top-level design
|
|
115
|
+
from which to extract parts.
|
|
116
|
+
doc:
|
|
117
|
+
The primary :class:`sbol2.Document` containing the base component definitions
|
|
118
|
+
and combinatorial derivations.
|
|
119
|
+
plasmid_doc:
|
|
120
|
+
An additional :class:`sbol2.Document` used to resolve component variants
|
|
121
|
+
(plasmid-specific variants referenced by combinatorial derivations).
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Dict[str, List[sbol2.ComponentDefinition]]:
|
|
125
|
+
A dictionary mapping component identities to lists
|
|
126
|
+
of variable component definitions.
|
|
127
|
+
|
|
128
|
+
- Sequential design components map to lists containing a single definition.
|
|
129
|
+
- Combinatorial variable components map to lists of variant definitions.
|
|
130
|
+
"""
|
|
131
|
+
component_list = [c for c in design.getInSequentialOrder()]
|
|
132
|
+
component_dict = {
|
|
133
|
+
component.identity: [doc.getComponentDefinition(component.definition)]
|
|
134
|
+
for component in component_list
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for deriv in doc.combinatorialderivations:
|
|
138
|
+
for component in deriv.variableComponents:
|
|
139
|
+
component_dict[component.variable] = [
|
|
140
|
+
plasmid_doc.getComponentDefinition(var) for var in component.variants
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
return component_dict
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def extract_toplevel_definition(doc: sbol2.Document) -> sbol2.ComponentDefinition:
|
|
147
|
+
return doc.componentDefinitions[0]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def enumerate_design_variants(component_dict):
|
|
151
|
+
"""
|
|
152
|
+
Given a dict mapping variable component identities to lists of ComponentDefinitions,
|
|
153
|
+
generate all possible design combinations as lists of ComponentDefinitions
|
|
154
|
+
(in consistent order of keys).
|
|
155
|
+
"""
|
|
156
|
+
keys = list(component_dict.keys())
|
|
157
|
+
variant_lists = [component_dict[k] for k in keys]
|
|
158
|
+
|
|
159
|
+
# Cartesian product across all variant lists
|
|
160
|
+
all_variants = list(itertools.product(*variant_lists))
|
|
161
|
+
|
|
162
|
+
all_variants = [list(combo) for combo in all_variants]
|
|
163
|
+
|
|
164
|
+
return all_variants
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def construct_plasmid_dict(
|
|
168
|
+
part_list: List[sbol2.ComponentDefinition], plasmid_collection: sbol2.Document
|
|
169
|
+
) -> Dict[str, List[MocloPlasmid]]:
|
|
170
|
+
"""
|
|
171
|
+
Builds a mapping from part display IDs to lists of compatible MoCloPlasmid objects.
|
|
172
|
+
|
|
173
|
+
For each part in the given list, this function searches the provided plasmid
|
|
174
|
+
collection for plasmids that contain the part as a component.
|
|
175
|
+
Each matching plasmid is wrapped in a `MocloPlasmid` object and added to the
|
|
176
|
+
dictionary under the part's display ID.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
part_list:
|
|
180
|
+
List of :class:`sbol2.ComponentDefinition` objects representing
|
|
181
|
+
the parts to match.
|
|
182
|
+
plasmid_collection:
|
|
183
|
+
The :class:`sbol2.Document` containing plasmids to search through.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Dict[str, List[MocloPlasmid]]:
|
|
187
|
+
A dictionary mapping each part display ID to a list of corresponding
|
|
188
|
+
`MocloPlasmid` objects found in the collection.
|
|
189
|
+
"""
|
|
190
|
+
plasmid_dict = {}
|
|
191
|
+
for part in part_list:
|
|
192
|
+
for plasmid in plasmid_collection.componentDefinitions:
|
|
193
|
+
if "http://identifiers.org/so/SO:0000637" in plasmid.roles:
|
|
194
|
+
for component in plasmid.components:
|
|
195
|
+
if (
|
|
196
|
+
component.definition == str(part)
|
|
197
|
+
): # TODO make sure this is not a composite plasmid, i.e. plasmid just contains singular part of interest
|
|
198
|
+
fusion_sites = [
|
|
199
|
+
site.name
|
|
200
|
+
for site in extract_fusion_sites(
|
|
201
|
+
plasmid, plasmid_collection
|
|
202
|
+
)
|
|
203
|
+
]
|
|
204
|
+
print(
|
|
205
|
+
f"found: {component.definition} in {plasmid} with {fusion_sites}"
|
|
206
|
+
) # TODO switch to logger for backend tracing?
|
|
207
|
+
plasmid_dict.setdefault(part.displayId, [])
|
|
208
|
+
|
|
209
|
+
componentName = plasmid_collection.getComponentDefinition(
|
|
210
|
+
component.definition
|
|
211
|
+
).name
|
|
212
|
+
|
|
213
|
+
plasmid_dict[part.displayId].append(
|
|
214
|
+
MocloPlasmid(componentName, plasmid, plasmid_collection)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return plasmid_dict
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def get_compatible_plasmids(
|
|
221
|
+
plasmid_dict: Dict[str, List[MocloPlasmid]], backbone: MocloPlasmid
|
|
222
|
+
) -> List[MocloPlasmid]:
|
|
223
|
+
"""
|
|
224
|
+
Returns a list of MocloPlasmid objects that can form a compatible assembly
|
|
225
|
+
with the given backbone plasmid. The function selects one plasmid from each
|
|
226
|
+
entry in the dictionary, ensuring that adjacent plasmids have matching MoClo fusion sites,
|
|
227
|
+
and that the first and last plasmids are compatible with the backbone.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
plasmid_dict: A dictionary mapping assembly positions or categories to lists
|
|
231
|
+
of MocloPlasmid objects.
|
|
232
|
+
backbone: The backbone MocloPlasmid whose fusion sites define compatibility.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
A list of compatible MocloPlasmid objects forming a sequential assembly.
|
|
236
|
+
"""
|
|
237
|
+
selected_plasmids = []
|
|
238
|
+
match_to = backbone
|
|
239
|
+
match_idx = 0
|
|
240
|
+
|
|
241
|
+
for i, key in enumerate(plasmid_dict):
|
|
242
|
+
for plasmid in plasmid_dict[key]:
|
|
243
|
+
if (
|
|
244
|
+
i == len(plasmid_dict) - 1
|
|
245
|
+
and plasmid.fusion_sites[0] == match_to.fusion_sites[match_idx]
|
|
246
|
+
and plasmid.fusion_sites[1] == backbone.fusion_sites[1]
|
|
247
|
+
):
|
|
248
|
+
print(
|
|
249
|
+
f"matched final component {plasmid.name} with {match_to.name} and {backbone.name} on fusion sites ({plasmid.fusion_sites[0]}, {plasmid.fusion_sites[1]})!"
|
|
250
|
+
)
|
|
251
|
+
selected_plasmids.append(plasmid)
|
|
252
|
+
break
|
|
253
|
+
elif (
|
|
254
|
+
i < len(plasmid_dict) - 1
|
|
255
|
+
and plasmid.fusion_sites[0] == match_to.fusion_sites[match_idx]
|
|
256
|
+
): # TODO add error handling if no compatible plasmid found
|
|
257
|
+
print(
|
|
258
|
+
f"matched {plasmid.name} with {match_to.name} on fusion site {plasmid.fusion_sites[0]}!"
|
|
259
|
+
)
|
|
260
|
+
selected_plasmids.append(plasmid)
|
|
261
|
+
match_to = plasmid
|
|
262
|
+
match_idx = 1
|
|
263
|
+
break
|
|
264
|
+
# TODO edge case where second fusion site does not match terminator fusion site will not be caught by current logic
|
|
265
|
+
# 10/14: rethink implementation, will likely need to be different for combinatorial designs
|
|
266
|
+
|
|
267
|
+
return selected_plasmids
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def translate_abstract_to_plasmids(
|
|
271
|
+
abstract_design: Union[sbol2.ComponentDefinition, sbol2.CombinatorialDerivation],
|
|
272
|
+
plasmid_collection: sbol2.Collection,
|
|
273
|
+
acceptor_backbone: sbol2.Document,
|
|
274
|
+
) -> List[MocloPlasmid]:
|
|
275
|
+
"""
|
|
276
|
+
Translates an abstract SBOLCanvas design into a set of compatible MoClo plasmid assemblies.
|
|
277
|
+
|
|
278
|
+
Takes an abstract design, identifies the appropriate component
|
|
279
|
+
definitions and combinatorial derivations, and produces all possible plasmid
|
|
280
|
+
combinations that can be assembled using the provided backbone and plasmid
|
|
281
|
+
collection.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
abstract_design_doc:
|
|
285
|
+
The :class:`sbol2.Document` representing the abstract genetic design.
|
|
286
|
+
May include either a single component definition (generic design) or
|
|
287
|
+
one or more combinatorial derivations (combinatorial design).
|
|
288
|
+
plasmid_collection:
|
|
289
|
+
The :class:`sbol2.Document` containing the available MoClo plasmid
|
|
290
|
+
components used for matching and assembly.
|
|
291
|
+
backbone_doc:
|
|
292
|
+
The :class:`sbol2.Document` defining the backbone plasmid into which
|
|
293
|
+
parts are assembled.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
List[MocloPlasmid]:
|
|
297
|
+
- For combinatorial designs: a list of unique compatible plasmids
|
|
298
|
+
(`MocloPlasmid` objects) representing all enumerated design variants.
|
|
299
|
+
- For generic designs: a list of compatible plasmids for the single
|
|
300
|
+
design instance.
|
|
301
|
+
"""
|
|
302
|
+
backbone_def = extract_toplevel_definition(acceptor_backbone)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
backbone_plasmid = MocloPlasmid(backbone_def.displayId, backbone_def, acceptor_backbone)
|
|
306
|
+
|
|
307
|
+
# combinatorial design
|
|
308
|
+
if len(abstract_design.combinatorialderivations) > 0:
|
|
309
|
+
abstract_design_def = abstract_design.getComponentDefinition(
|
|
310
|
+
abstract_design.combinatorialderivations[0].masterTemplate
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
combinatorial_part_dict = extract_combinatorial_design_parts(
|
|
314
|
+
abstract_design_def, abstract_design , plasmid_collection
|
|
315
|
+
)
|
|
316
|
+
enumerated_part_list = enumerate_design_variants(combinatorial_part_dict)
|
|
317
|
+
|
|
318
|
+
seen = set()
|
|
319
|
+
ordered_unique_plasmids = []
|
|
320
|
+
|
|
321
|
+
for design in enumerated_part_list:
|
|
322
|
+
plasmid_dict = construct_plasmid_dict(design, plasmid_collection)
|
|
323
|
+
compatible_plasmids = get_compatible_plasmids(
|
|
324
|
+
plasmid_dict, backbone_plasmid
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
for plasmid in compatible_plasmids:
|
|
328
|
+
if plasmid not in seen:
|
|
329
|
+
seen.add(plasmid)
|
|
330
|
+
ordered_unique_plasmids.append(plasmid)
|
|
331
|
+
|
|
332
|
+
return ordered_unique_plasmids
|
|
333
|
+
|
|
334
|
+
# generic design
|
|
335
|
+
else:
|
|
336
|
+
abstract_design_def = extract_toplevel_definition(abstract_design)
|
|
337
|
+
|
|
338
|
+
ordered_part_definitions = extract_design_parts(
|
|
339
|
+
abstract_design_def, abstract_design
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
plasmid_dict = construct_plasmid_dict(
|
|
343
|
+
ordered_part_definitions, plasmid_collection
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
return get_compatible_plasmids(plasmid_dict, backbone_plasmid)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import sbol2
|
|
2
|
+
from typing import Union, List
|
|
3
|
+
import zipfile
|
|
4
|
+
from buildcompiler.abstract_translator import translate_abstract_to_plasmids
|
|
5
|
+
from buildcompiler.sbol2build import golden_gate_assembly_plan
|
|
6
|
+
from buildcompiler.robotutils import assembly_plan_RDF_to_JSON, run_opentrons_script_with_json_to_zip
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# function which input is an abstract design and output build specifications by creating an assembly plan, and a zip file with a run_sbol2assembly.py, an automated_assembly_log.txt, assemblyplan_output.JSON, and assembly_protocol.xlsx
|
|
10
|
+
|
|
11
|
+
def assembly_compiler(document: sbol2.Document,
|
|
12
|
+
abstract_design: str,
|
|
13
|
+
plasmids_collection: str,
|
|
14
|
+
plasmid_acceptor_backbone: str,
|
|
15
|
+
files_path: str) -> zipfile.ZipFile:
|
|
16
|
+
"""
|
|
17
|
+
Compiles an abstract design into build specifications.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
abstract_design (Union[sbol2.Component, sbol2.CombinatorialDerivation]): The abstract design to be compiled.
|
|
21
|
+
specifications (sbol2.Component): The component to store the build specifications.
|
|
22
|
+
Returns:
|
|
23
|
+
zipfile.ZipFile: A zip file containing the build specifications and assembly plan.
|
|
24
|
+
"""
|
|
25
|
+
restriction_enzyme = "BsaI"
|
|
26
|
+
# Translate abstract design to plasmids
|
|
27
|
+
list_of_plasmids = translate_abstract_to_plasmids(abstract_design_doc = abstract_design,
|
|
28
|
+
plasmid_collection = plasmids_collection,
|
|
29
|
+
backbone_doc= plasmid_acceptor_backbone)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Create assembly plan
|
|
34
|
+
assembly_plan = golden_gate_assembly_plan(name = "Assembly_Plan",
|
|
35
|
+
parts_in_backbone= list_of_plasmids,
|
|
36
|
+
plasmid_acceptor_backbone= plasmid_acceptor_backbone,
|
|
37
|
+
restriction_enzyme= restriction_enzyme,
|
|
38
|
+
document= document)
|
|
39
|
+
|
|
40
|
+
# Generate build specifications JSON
|
|
41
|
+
build_specs_JSON = assembly_plan_RDF_to_JSON(assembly_plan)
|
|
42
|
+
|
|
43
|
+
# Create zip file with required files
|
|
44
|
+
zip_file = run_opentrons_script_with_json_to_zip(opentrons_script_path= files_path + "/run_sbol2assembly_libre.py",
|
|
45
|
+
json_file_path= files_path + "/assemblyplan_output.json",
|
|
46
|
+
zip_name= "buildcompiler.zip",
|
|
47
|
+
overwrite= True)
|
|
48
|
+
|
|
49
|
+
return assembly_plan, zip_file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
FUSION_SITES = {
|
|
2
|
+
"A": "GGAG",
|
|
3
|
+
"B": "TACT",
|
|
4
|
+
"C": "AATG",
|
|
5
|
+
"D": "AGGT",
|
|
6
|
+
"E": "GCTT",
|
|
7
|
+
"F": "CGCT",
|
|
8
|
+
"G": "TGCC",
|
|
9
|
+
"H": "ACTA",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
DNA_TYPES = { # TODO see about restricting dna types to only accept dna
|
|
13
|
+
"http://www.biopax.org/release/biopax-level3.owl#Dna",
|
|
14
|
+
"http://www.biopax.org/release/biopax-level3.owl#DnaRegion",
|
|
15
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import sbol2
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
import zipfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
def assembly_plan_RDF_to_JSON(file):
|
|
11
|
+
if type(file)==sbol2.Document:
|
|
12
|
+
doc = file
|
|
13
|
+
else:
|
|
14
|
+
sbol2.Config.setOption('sbol_typed_uris', False)
|
|
15
|
+
doc = sbol2.Document()
|
|
16
|
+
doc.read(file)
|
|
17
|
+
|
|
18
|
+
# Known SO roles
|
|
19
|
+
PRODUCT_ROLE = 'http://identifiers.org/so/SO:0000804'
|
|
20
|
+
BackBone_ROLE = 'http://identifiers.org/so/SO:0000755'
|
|
21
|
+
ENZYME_ROLE = 'http://identifiers.org/obi/OBI:0000732'
|
|
22
|
+
|
|
23
|
+
PARTS_ROLE_LIST = [
|
|
24
|
+
'http://identifiers.org/so/SO:0000031', 'http://identifiers.org/so/SO:0000316',
|
|
25
|
+
'http://identifiers.org/so/SO:0001977', 'http://identifiers.org/so/SO:0001956',
|
|
26
|
+
'http://identifiers.org/so/SO:0000188', 'http://identifiers.org/so/SO:0000839',
|
|
27
|
+
'http://identifiers.org/so/SO:0000167', 'http://identifiers.org/so/SO:0000139',
|
|
28
|
+
'http://identifiers.org/so/SO:0001979', 'http://identifiers.org/so/SO:0001955',
|
|
29
|
+
'http://identifiers.org/so/SO:0001546', 'http://identifiers.org/so/SO:0001263',
|
|
30
|
+
'http://identifiers.org/SO:0000141', 'http://identifiers.org/so/SO:0000141'
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
product_dicts = []
|
|
34
|
+
globalEnzyme = None
|
|
35
|
+
|
|
36
|
+
for cd in doc.componentDefinitions:
|
|
37
|
+
print(f"\n🔍 Checking Component: {cd.displayId}")
|
|
38
|
+
print(f" Types: {cd.types}")
|
|
39
|
+
print(f" Roles: {cd.roles}")
|
|
40
|
+
|
|
41
|
+
if ENZYME_ROLE in cd.roles:
|
|
42
|
+
globalEnzyme = cd.identity
|
|
43
|
+
print(f"✅ Found enzyme definition: {globalEnzyme}")
|
|
44
|
+
|
|
45
|
+
if PRODUCT_ROLE in cd.roles:
|
|
46
|
+
result = {
|
|
47
|
+
'Product': cd.identity,
|
|
48
|
+
'Backbone': None,
|
|
49
|
+
'PartsList': [],
|
|
50
|
+
'Restriction Enzyme': None
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for comp in cd.components:
|
|
54
|
+
sub_cd = doc.componentDefinitions.get(comp.definition)
|
|
55
|
+
if sub_cd is None:
|
|
56
|
+
print(f"⚠️ Component definition for {comp.displayId} not found.")
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
print(f" → Subcomponent: {sub_cd.displayId}")
|
|
60
|
+
print(f" Roles: {sub_cd.roles}")
|
|
61
|
+
|
|
62
|
+
if BackBone_ROLE in sub_cd.roles:
|
|
63
|
+
result['Backbone'] = sub_cd.identity
|
|
64
|
+
print(f" 🧬 Assigned Backbone: {sub_cd.identity}")
|
|
65
|
+
|
|
66
|
+
if any(role in PARTS_ROLE_LIST for role in sub_cd.roles):
|
|
67
|
+
result['PartsList'].append(sub_cd.identity)
|
|
68
|
+
print(f" 🧩 Added Part: {sub_cd.identity}")
|
|
69
|
+
|
|
70
|
+
if not result['Backbone']:
|
|
71
|
+
print(f"⚠️ No backbone found for product {cd.displayId}")
|
|
72
|
+
if not result['PartsList']:
|
|
73
|
+
print(f"⚠️ No parts found for product {cd.displayId}")
|
|
74
|
+
|
|
75
|
+
product_dicts.append(result)
|
|
76
|
+
|
|
77
|
+
for entry in product_dicts:
|
|
78
|
+
entry['Restriction Enzyme'] = globalEnzyme
|
|
79
|
+
|
|
80
|
+
with open('output.json', 'w') as json_file:
|
|
81
|
+
json.dump(product_dicts, json_file, indent=4)
|
|
82
|
+
|
|
83
|
+
return product_dicts
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def run_opentrons_script_with_json_to_zip(
|
|
87
|
+
opentrons_script_path: str,
|
|
88
|
+
json_file_path: str,
|
|
89
|
+
zip_name: str | None = None,
|
|
90
|
+
overwrite: bool = False,
|
|
91
|
+
) -> Path:
|
|
92
|
+
"""
|
|
93
|
+
Runs `opentrons_simulate` on an Opentrons script + JSON, captures stdout/stderr,
|
|
94
|
+
and writes a ZIP file *next to the original opentrons script*.
|
|
95
|
+
|
|
96
|
+
Returns: Path to the created zip file.
|
|
97
|
+
"""
|
|
98
|
+
script_path = Path(opentrons_script_path).resolve()
|
|
99
|
+
json_path = Path(json_file_path).resolve()
|
|
100
|
+
|
|
101
|
+
if not script_path.exists():
|
|
102
|
+
raise FileNotFoundError(f"Opentrons script not found: {script_path}")
|
|
103
|
+
if not json_path.exists():
|
|
104
|
+
raise FileNotFoundError(f"JSON file not found: {json_path}")
|
|
105
|
+
|
|
106
|
+
out_dir = script_path.parent
|
|
107
|
+
base_name = zip_name or f"{script_path.stem}_opentrons_simulation.zip"
|
|
108
|
+
out_zip = out_dir / base_name
|
|
109
|
+
|
|
110
|
+
if out_zip.exists() and not overwrite:
|
|
111
|
+
# avoid clobbering: foo.zip -> foo_1.zip -> foo_2.zip ...
|
|
112
|
+
stem = out_zip.stem
|
|
113
|
+
suffix = out_zip.suffix
|
|
114
|
+
i = 1
|
|
115
|
+
while True:
|
|
116
|
+
candidate = out_dir / f"{stem}_{i}{suffix}"
|
|
117
|
+
if not candidate.exists():
|
|
118
|
+
out_zip = candidate
|
|
119
|
+
break
|
|
120
|
+
i += 1
|
|
121
|
+
|
|
122
|
+
with tempfile.TemporaryDirectory() as tmpdirname:
|
|
123
|
+
tmpdir = Path(tmpdirname)
|
|
124
|
+
|
|
125
|
+
# Copy inputs into temp dir
|
|
126
|
+
tmp_script = tmpdir / script_path.name
|
|
127
|
+
tmp_json = tmpdir / json_path.name
|
|
128
|
+
shutil.copy2(script_path, tmp_script)
|
|
129
|
+
shutil.copy2(json_path, tmp_json)
|
|
130
|
+
|
|
131
|
+
# Run inside temp dir so relative-path outputs land in tmpdir (and get zipped)
|
|
132
|
+
|
|
133
|
+
# Run script (which has opentrons script hardcoded) using JSON file
|
|
134
|
+
log = subprocess.run(["opentrons_simulate", opentrons_script_path, json_file_path], capture_output=True).stdout
|
|
135
|
+
|
|
136
|
+
# Save log to a file in the temporary directory
|
|
137
|
+
with open(os.path.join(tmpdir, "build_log.txt"), "wb") as log_file:
|
|
138
|
+
log_file.write(log)
|
|
139
|
+
|
|
140
|
+
# Always include logs in the zip
|
|
141
|
+
#(tmpdir / "simulate_stdout.txt").write_text(proc.stdout or "", encoding="utf-8", errors="replace")
|
|
142
|
+
#(tmpdir / "simulate_stderr.txt").write_text(proc.stderr or "", encoding="utf-8", errors="replace")
|
|
143
|
+
#(tmpdir / "simulate_returncode.txt").write_text(str(proc.returncode), encoding="utf-8")
|
|
144
|
+
|
|
145
|
+
# Create the ZIP on disk next to the original script
|
|
146
|
+
with zipfile.ZipFile(out_zip, "w", compression=zipfile.ZIP_DEFLATED) as z:
|
|
147
|
+
for p in tmpdir.rglob("*"):
|
|
148
|
+
if p.is_file():
|
|
149
|
+
z.write(p, arcname=p.relative_to(tmpdir))
|
|
150
|
+
|
|
151
|
+
return out_zip
|