molbuilder 1.0.0__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.
- molbuilder/__init__.py +8 -0
- molbuilder/__main__.py +6 -0
- molbuilder/atomic/__init__.py +4 -0
- molbuilder/atomic/bohr.py +235 -0
- molbuilder/atomic/quantum_atom.py +334 -0
- molbuilder/atomic/quantum_numbers.py +196 -0
- molbuilder/atomic/wavefunctions.py +297 -0
- molbuilder/bonding/__init__.py +4 -0
- molbuilder/bonding/covalent.py +442 -0
- molbuilder/bonding/lewis.py +347 -0
- molbuilder/bonding/vsepr.py +433 -0
- molbuilder/cli/__init__.py +1 -0
- molbuilder/cli/demos.py +516 -0
- molbuilder/cli/menu.py +127 -0
- molbuilder/cli/wizard.py +831 -0
- molbuilder/core/__init__.py +6 -0
- molbuilder/core/bond_data.py +170 -0
- molbuilder/core/constants.py +51 -0
- molbuilder/core/element_properties.py +183 -0
- molbuilder/core/elements.py +181 -0
- molbuilder/core/geometry.py +232 -0
- molbuilder/gui/__init__.py +2 -0
- molbuilder/gui/app.py +286 -0
- molbuilder/gui/canvas3d.py +115 -0
- molbuilder/gui/dialogs.py +117 -0
- molbuilder/gui/event_handler.py +118 -0
- molbuilder/gui/sidebar.py +105 -0
- molbuilder/gui/toolbar.py +71 -0
- molbuilder/io/__init__.py +1 -0
- molbuilder/io/json_io.py +146 -0
- molbuilder/io/mol_sdf.py +169 -0
- molbuilder/io/pdb.py +184 -0
- molbuilder/io/smiles_io.py +47 -0
- molbuilder/io/xyz.py +103 -0
- molbuilder/molecule/__init__.py +2 -0
- molbuilder/molecule/amino_acids.py +919 -0
- molbuilder/molecule/builders.py +257 -0
- molbuilder/molecule/conformations.py +70 -0
- molbuilder/molecule/functional_groups.py +484 -0
- molbuilder/molecule/graph.py +712 -0
- molbuilder/molecule/peptides.py +13 -0
- molbuilder/molecule/stereochemistry.py +6 -0
- molbuilder/process/__init__.py +3 -0
- molbuilder/process/conditions.py +260 -0
- molbuilder/process/costing.py +316 -0
- molbuilder/process/purification.py +285 -0
- molbuilder/process/reactor.py +297 -0
- molbuilder/process/safety.py +476 -0
- molbuilder/process/scale_up.py +427 -0
- molbuilder/process/solvent_systems.py +204 -0
- molbuilder/reactions/__init__.py +3 -0
- molbuilder/reactions/functional_group_detect.py +728 -0
- molbuilder/reactions/knowledge_base.py +1716 -0
- molbuilder/reactions/reaction_types.py +102 -0
- molbuilder/reactions/reagent_data.py +1248 -0
- molbuilder/reactions/retrosynthesis.py +1430 -0
- molbuilder/reactions/synthesis_route.py +377 -0
- molbuilder/reports/__init__.py +158 -0
- molbuilder/reports/cost_report.py +206 -0
- molbuilder/reports/molecule_report.py +279 -0
- molbuilder/reports/safety_report.py +296 -0
- molbuilder/reports/synthesis_report.py +283 -0
- molbuilder/reports/text_formatter.py +170 -0
- molbuilder/smiles/__init__.py +4 -0
- molbuilder/smiles/parser.py +487 -0
- molbuilder/smiles/tokenizer.py +291 -0
- molbuilder/smiles/writer.py +375 -0
- molbuilder/visualization/__init__.py +1 -0
- molbuilder/visualization/bohr_viz.py +166 -0
- molbuilder/visualization/molecule_viz.py +368 -0
- molbuilder/visualization/quantum_viz.py +434 -0
- molbuilder/visualization/theme.py +12 -0
- molbuilder-1.0.0.dist-info/METADATA +360 -0
- molbuilder-1.0.0.dist-info/RECORD +78 -0
- molbuilder-1.0.0.dist-info/WHEEL +5 -0
- molbuilder-1.0.0.dist-info/entry_points.txt +2 -0
- molbuilder-1.0.0.dist-info/licenses/LICENSE +21 -0
- molbuilder-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Forward synthesis route planning from retrosynthesis results.
|
|
2
|
+
|
|
3
|
+
This module traverses a ``RetrosynthesisTree`` (produced by the
|
|
4
|
+
retrosynthetic analysis engine) along its best disconnections and
|
|
5
|
+
reverses the order to produce a step-by-step forward synthesis route.
|
|
6
|
+
|
|
7
|
+
Key public functions
|
|
8
|
+
--------------------
|
|
9
|
+
extract_best_route(tree) -> SynthesisRoute
|
|
10
|
+
format_route(route) -> str
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
|
|
17
|
+
from molbuilder.reactions.retrosynthesis import (
|
|
18
|
+
RetrosynthesisTree,
|
|
19
|
+
RetroNode,
|
|
20
|
+
Precursor,
|
|
21
|
+
PURCHASABLE_MATERIALS,
|
|
22
|
+
is_purchasable,
|
|
23
|
+
)
|
|
24
|
+
from molbuilder.reactions.reaction_types import ReactionTemplate
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# =====================================================================
|
|
28
|
+
# Data structures
|
|
29
|
+
# =====================================================================
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class SynthesisStep:
|
|
33
|
+
"""A single step in a forward synthesis route.
|
|
34
|
+
|
|
35
|
+
Attributes
|
|
36
|
+
----------
|
|
37
|
+
step_number : int
|
|
38
|
+
Sequential step number (1-based).
|
|
39
|
+
template : ReactionTemplate
|
|
40
|
+
The reaction template used in this step.
|
|
41
|
+
precursors : list[Precursor]
|
|
42
|
+
Starting materials / intermediates consumed.
|
|
43
|
+
product_smiles : str
|
|
44
|
+
SMILES of the product formed in this step.
|
|
45
|
+
product_name : str
|
|
46
|
+
Human-readable name for the product.
|
|
47
|
+
conditions : str
|
|
48
|
+
Summary of temperature, solvent, and catalyst.
|
|
49
|
+
expected_yield : float
|
|
50
|
+
Estimated isolated yield (percent), midpoint of template range.
|
|
51
|
+
notes : str
|
|
52
|
+
Safety or practical notes.
|
|
53
|
+
"""
|
|
54
|
+
step_number: int
|
|
55
|
+
template: ReactionTemplate
|
|
56
|
+
precursors: list[Precursor]
|
|
57
|
+
product_smiles: str
|
|
58
|
+
product_name: str
|
|
59
|
+
conditions: str
|
|
60
|
+
expected_yield: float
|
|
61
|
+
notes: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class SynthesisRoute:
|
|
66
|
+
"""A complete forward synthesis route.
|
|
67
|
+
|
|
68
|
+
Attributes
|
|
69
|
+
----------
|
|
70
|
+
target_smiles : str
|
|
71
|
+
SMILES of the final target molecule.
|
|
72
|
+
target_name : str
|
|
73
|
+
Human-readable name (from molecule or SMILES).
|
|
74
|
+
steps : list[SynthesisStep]
|
|
75
|
+
Ordered list of synthesis steps (first step uses only
|
|
76
|
+
purchasable materials).
|
|
77
|
+
overall_yield : float
|
|
78
|
+
Product of individual step yields (percent).
|
|
79
|
+
starting_materials : list[Precursor]
|
|
80
|
+
All purchasable starting materials needed.
|
|
81
|
+
total_steps : int
|
|
82
|
+
Total number of steps in the route.
|
|
83
|
+
longest_linear_sequence : int
|
|
84
|
+
Length of the longest linear chain of dependent steps.
|
|
85
|
+
"""
|
|
86
|
+
target_smiles: str
|
|
87
|
+
target_name: str
|
|
88
|
+
steps: list[SynthesisStep] = field(default_factory=list)
|
|
89
|
+
overall_yield: float = 0.0
|
|
90
|
+
starting_materials: list[Precursor] = field(default_factory=list)
|
|
91
|
+
total_steps: int = 0
|
|
92
|
+
longest_linear_sequence: int = 0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# =====================================================================
|
|
96
|
+
# Conditions summary builder
|
|
97
|
+
# =====================================================================
|
|
98
|
+
|
|
99
|
+
def _build_conditions(template: ReactionTemplate) -> str:
|
|
100
|
+
"""Build a one-line conditions summary from a ReactionTemplate.
|
|
101
|
+
|
|
102
|
+
Includes temperature range, preferred solvent, and catalyst if any.
|
|
103
|
+
"""
|
|
104
|
+
parts: list[str] = []
|
|
105
|
+
|
|
106
|
+
# Temperature
|
|
107
|
+
lo, hi = template.temperature_range
|
|
108
|
+
if lo == hi:
|
|
109
|
+
parts.append(f"{lo:.0f} C")
|
|
110
|
+
else:
|
|
111
|
+
parts.append(f"{lo:.0f} to {hi:.0f} C")
|
|
112
|
+
|
|
113
|
+
# Solvent
|
|
114
|
+
if template.solvents:
|
|
115
|
+
parts.append(template.solvents[0])
|
|
116
|
+
|
|
117
|
+
# Catalyst
|
|
118
|
+
if template.catalysts:
|
|
119
|
+
parts.append(f"cat. {template.catalysts[0]}")
|
|
120
|
+
|
|
121
|
+
# Reagents (abbreviated)
|
|
122
|
+
if template.reagents:
|
|
123
|
+
abbreviated = template.reagents[:2]
|
|
124
|
+
parts.append(" + ".join(abbreviated))
|
|
125
|
+
|
|
126
|
+
return "; ".join(parts)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _expected_yield(template: ReactionTemplate) -> float:
|
|
130
|
+
"""Midpoint of the template's typical yield range."""
|
|
131
|
+
lo, hi = template.typical_yield
|
|
132
|
+
return (lo + hi) / 2.0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _product_name(smiles: str) -> str:
|
|
136
|
+
"""Return a human-readable name for a SMILES string if known."""
|
|
137
|
+
entry = PURCHASABLE_MATERIALS.get(smiles)
|
|
138
|
+
if entry is not None:
|
|
139
|
+
return entry[0]
|
|
140
|
+
return smiles
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# =====================================================================
|
|
144
|
+
# Tree traversal: collect steps in reverse (retro -> forward)
|
|
145
|
+
# =====================================================================
|
|
146
|
+
|
|
147
|
+
def _collect_retro_steps(
|
|
148
|
+
node: RetroNode,
|
|
149
|
+
steps_accumulator: list[tuple[RetroNode, ReactionTemplate, list[Precursor]]],
|
|
150
|
+
visited: set[str],
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Walk the retrosynthesis tree depth-first, collecting steps.
|
|
153
|
+
|
|
154
|
+
Each non-leaf, non-purchasable node with a best_disconnection
|
|
155
|
+
contributes one step. Children are visited first (depth-first)
|
|
156
|
+
so that when the list is later reversed, leaf-level reactions
|
|
157
|
+
come first in the forward direction.
|
|
158
|
+
"""
|
|
159
|
+
if node.smiles in visited:
|
|
160
|
+
return
|
|
161
|
+
visited.add(node.smiles)
|
|
162
|
+
|
|
163
|
+
if node.is_purchasable:
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
# First recurse into children (precursors)
|
|
167
|
+
for child in node.children:
|
|
168
|
+
_collect_retro_steps(child, steps_accumulator, visited)
|
|
169
|
+
|
|
170
|
+
# Then record this node's disconnection
|
|
171
|
+
if node.best_disconnection is not None:
|
|
172
|
+
steps_accumulator.append((
|
|
173
|
+
node,
|
|
174
|
+
node.best_disconnection.template,
|
|
175
|
+
node.best_disconnection.precursors,
|
|
176
|
+
))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _gather_purchasable_leaves(node: RetroNode, result: list[Precursor],
|
|
180
|
+
seen: set[str]) -> None:
|
|
181
|
+
"""Collect all purchasable leaf nodes as Precursor objects."""
|
|
182
|
+
if node.is_purchasable:
|
|
183
|
+
if node.smiles not in seen:
|
|
184
|
+
seen.add(node.smiles)
|
|
185
|
+
entry = PURCHASABLE_MATERIALS.get(node.smiles)
|
|
186
|
+
name = entry[0] if entry else node.smiles
|
|
187
|
+
cost = entry[1] if entry else 50.0
|
|
188
|
+
result.append(Precursor(
|
|
189
|
+
smiles=node.smiles, molecule=None,
|
|
190
|
+
name=name, cost_per_kg=cost,
|
|
191
|
+
))
|
|
192
|
+
return
|
|
193
|
+
for child in node.children:
|
|
194
|
+
_gather_purchasable_leaves(child, result, seen)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _compute_longest_linear(node: RetroNode) -> int:
|
|
198
|
+
"""Compute the longest linear sequence of steps from the root.
|
|
199
|
+
|
|
200
|
+
The longest linear sequence is the depth of the deepest non-
|
|
201
|
+
purchasable node.
|
|
202
|
+
"""
|
|
203
|
+
if node.is_purchasable or not node.children:
|
|
204
|
+
return 0
|
|
205
|
+
return 1 + max(_compute_longest_linear(c) for c in node.children)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# =====================================================================
|
|
209
|
+
# Public API
|
|
210
|
+
# =====================================================================
|
|
211
|
+
|
|
212
|
+
def extract_best_route(tree: RetrosynthesisTree) -> SynthesisRoute:
|
|
213
|
+
"""Extract the best forward synthesis route from a retrosynthesis tree.
|
|
214
|
+
|
|
215
|
+
Traverses the tree along ``best_disconnection`` links, reverses the
|
|
216
|
+
order to produce a forward synthesis plan, and computes the overall
|
|
217
|
+
yield as the product of individual step yields.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
tree : RetrosynthesisTree
|
|
222
|
+
The retrosynthesis result from ``retrosynthesis()``.
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
SynthesisRoute
|
|
227
|
+
A forward synthesis route with ordered steps, starting from
|
|
228
|
+
purchasable materials and ending at the target.
|
|
229
|
+
"""
|
|
230
|
+
root = tree.target
|
|
231
|
+
|
|
232
|
+
# Collect retro steps (deepest first)
|
|
233
|
+
retro_steps: list[tuple[RetroNode, ReactionTemplate, list[Precursor]]] = []
|
|
234
|
+
visited: set[str] = set()
|
|
235
|
+
_collect_retro_steps(root, retro_steps, visited)
|
|
236
|
+
|
|
237
|
+
# retro_steps are already in forward order because we recurse into
|
|
238
|
+
# children before appending the parent. If we had reversed, the
|
|
239
|
+
# deepest transformations would come first (correct for forward
|
|
240
|
+
# synthesis). Since _collect_retro_steps already visits children
|
|
241
|
+
# first, the list is naturally in forward order.
|
|
242
|
+
|
|
243
|
+
# Build SynthesisStep objects
|
|
244
|
+
steps: list[SynthesisStep] = []
|
|
245
|
+
overall_yield = 100.0
|
|
246
|
+
|
|
247
|
+
for i, (node, template, precursors) in enumerate(retro_steps):
|
|
248
|
+
step_yield = _expected_yield(template)
|
|
249
|
+
overall_yield *= (step_yield / 100.0)
|
|
250
|
+
|
|
251
|
+
notes_parts: list[str] = []
|
|
252
|
+
if template.safety_notes:
|
|
253
|
+
notes_parts.append(template.safety_notes)
|
|
254
|
+
if template.scale_notes:
|
|
255
|
+
notes_parts.append(template.scale_notes)
|
|
256
|
+
notes = " ".join(notes_parts) if notes_parts else ""
|
|
257
|
+
|
|
258
|
+
step = SynthesisStep(
|
|
259
|
+
step_number=i + 1,
|
|
260
|
+
template=template,
|
|
261
|
+
precursors=precursors,
|
|
262
|
+
product_smiles=node.smiles,
|
|
263
|
+
product_name=_product_name(node.smiles),
|
|
264
|
+
conditions=_build_conditions(template),
|
|
265
|
+
expected_yield=step_yield,
|
|
266
|
+
notes=notes,
|
|
267
|
+
)
|
|
268
|
+
steps.append(step)
|
|
269
|
+
|
|
270
|
+
# Gather all purchasable starting materials
|
|
271
|
+
starting_materials: list[Precursor] = []
|
|
272
|
+
seen_sm: set[str] = set()
|
|
273
|
+
_gather_purchasable_leaves(root, starting_materials, seen_sm)
|
|
274
|
+
|
|
275
|
+
# Longest linear sequence
|
|
276
|
+
lls = _compute_longest_linear(root)
|
|
277
|
+
|
|
278
|
+
target_name = _product_name(root.smiles)
|
|
279
|
+
|
|
280
|
+
return SynthesisRoute(
|
|
281
|
+
target_smiles=root.smiles,
|
|
282
|
+
target_name=target_name,
|
|
283
|
+
steps=steps,
|
|
284
|
+
overall_yield=overall_yield,
|
|
285
|
+
starting_materials=starting_materials,
|
|
286
|
+
total_steps=len(steps),
|
|
287
|
+
longest_linear_sequence=lls,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def format_route(route: SynthesisRoute) -> str:
|
|
292
|
+
"""Format a SynthesisRoute as a readable ASCII text summary.
|
|
293
|
+
|
|
294
|
+
Parameters
|
|
295
|
+
----------
|
|
296
|
+
route : SynthesisRoute
|
|
297
|
+
The synthesis route to format.
|
|
298
|
+
|
|
299
|
+
Returns
|
|
300
|
+
-------
|
|
301
|
+
str
|
|
302
|
+
Multi-line text summary of the route.
|
|
303
|
+
|
|
304
|
+
Example output::
|
|
305
|
+
|
|
306
|
+
============================================================
|
|
307
|
+
Forward Synthesis Route
|
|
308
|
+
============================================================
|
|
309
|
+
Target : CC(=O)OCC (ethyl acetate)
|
|
310
|
+
Steps : 1
|
|
311
|
+
Overall yield : 67.5%
|
|
312
|
+
Longest linear sequence : 1
|
|
313
|
+
============================================================
|
|
314
|
+
|
|
315
|
+
Starting Materials:
|
|
316
|
+
- CC(O)=O (acetic acid, $1.50/kg)
|
|
317
|
+
- CCO (ethanol, $2.00/kg)
|
|
318
|
+
|
|
319
|
+
------------------------------------------------------------
|
|
320
|
+
Step 1: Fischer esterification
|
|
321
|
+
------------------------------------------------------------
|
|
322
|
+
Precursors : CC(O)=O + CCO
|
|
323
|
+
Product : CC(=O)OCC
|
|
324
|
+
Conditions : 60 to 120 C; toluene (Dean-Stark); ...
|
|
325
|
+
Yield : 67.5%
|
|
326
|
+
Notes : ...
|
|
327
|
+
------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
Overall yield: 67.5%
|
|
330
|
+
============================================================
|
|
331
|
+
"""
|
|
332
|
+
sep = "=" * 60
|
|
333
|
+
thin_sep = "-" * 60
|
|
334
|
+
lines: list[str] = []
|
|
335
|
+
|
|
336
|
+
lines.append(sep)
|
|
337
|
+
lines.append("Forward Synthesis Route")
|
|
338
|
+
lines.append(sep)
|
|
339
|
+
lines.append(f"Target : {route.target_smiles} ({route.target_name})")
|
|
340
|
+
lines.append(f"Steps : {route.total_steps}")
|
|
341
|
+
lines.append(f"Overall yield : {route.overall_yield:.1f}%")
|
|
342
|
+
lines.append(f"Longest linear sequence : {route.longest_linear_sequence}")
|
|
343
|
+
lines.append(sep)
|
|
344
|
+
|
|
345
|
+
# Starting materials
|
|
346
|
+
lines.append("")
|
|
347
|
+
lines.append("Starting Materials:")
|
|
348
|
+
if route.starting_materials:
|
|
349
|
+
for sm in route.starting_materials:
|
|
350
|
+
lines.append(f" - {sm.smiles} ({sm.name}, ${sm.cost_per_kg:.2f}/kg)")
|
|
351
|
+
else:
|
|
352
|
+
lines.append(" (none identified)")
|
|
353
|
+
|
|
354
|
+
# Steps
|
|
355
|
+
for step in route.steps:
|
|
356
|
+
lines.append("")
|
|
357
|
+
lines.append(thin_sep)
|
|
358
|
+
lines.append(f"Step {step.step_number}: {step.template.name}")
|
|
359
|
+
if step.template.named_reaction:
|
|
360
|
+
lines.append(f" Named reaction : {step.template.named_reaction}")
|
|
361
|
+
lines.append(thin_sep)
|
|
362
|
+
|
|
363
|
+
precursor_str = " + ".join(p.smiles for p in step.precursors)
|
|
364
|
+
lines.append(f" Precursors : {precursor_str}")
|
|
365
|
+
lines.append(f" Product : {step.product_smiles}"
|
|
366
|
+
f" ({step.product_name})")
|
|
367
|
+
lines.append(f" Conditions : {step.conditions}")
|
|
368
|
+
lines.append(f" Yield : {step.expected_yield:.1f}%")
|
|
369
|
+
if step.notes:
|
|
370
|
+
lines.append(f" Notes : {step.notes}")
|
|
371
|
+
|
|
372
|
+
lines.append("")
|
|
373
|
+
lines.append(thin_sep)
|
|
374
|
+
lines.append(f"Overall yield: {route.overall_yield:.1f}%")
|
|
375
|
+
lines.append(sep)
|
|
376
|
+
|
|
377
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Report generation: synthesis, safety, costing, molecule summaries.
|
|
2
|
+
|
|
3
|
+
Public API
|
|
4
|
+
----------
|
|
5
|
+
Text formatting utilities:
|
|
6
|
+
section_header, subsection_header, ascii_table, word_wrap,
|
|
7
|
+
bullet_list, key_value_block, horizontal_bar,
|
|
8
|
+
format_currency, format_percent
|
|
9
|
+
|
|
10
|
+
Report generators:
|
|
11
|
+
generate_molecule_report -- comprehensive molecule analysis
|
|
12
|
+
generate_synthesis_report -- multi-step synthesis route
|
|
13
|
+
generate_safety_report -- hazard and PPE assessment
|
|
14
|
+
generate_cost_report -- cost breakdown with charts
|
|
15
|
+
|
|
16
|
+
Protocol types:
|
|
17
|
+
SynthesisStepLike -- structural type for synthesis step objects
|
|
18
|
+
SynthesisRouteLike -- structural type for synthesis route objects
|
|
19
|
+
CostEstimateLike -- structural type for cost estimation objects
|
|
20
|
+
SafetyAssessmentLike -- structural type for safety assessment objects
|
|
21
|
+
MoleculeLike -- structural type for molecule-like objects
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Protocol, runtime_checkable
|
|
27
|
+
|
|
28
|
+
from molbuilder.reports.text_formatter import (
|
|
29
|
+
section_header,
|
|
30
|
+
subsection_header,
|
|
31
|
+
ascii_table,
|
|
32
|
+
word_wrap,
|
|
33
|
+
bullet_list,
|
|
34
|
+
key_value_block,
|
|
35
|
+
horizontal_bar,
|
|
36
|
+
format_currency,
|
|
37
|
+
format_percent,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
from molbuilder.reports.molecule_report import generate_molecule_report
|
|
41
|
+
from molbuilder.reports.synthesis_report import generate_synthesis_report
|
|
42
|
+
from molbuilder.reports.safety_report import generate_safety_report
|
|
43
|
+
from molbuilder.reports.cost_report import generate_cost_report
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# =====================================================================
|
|
47
|
+
# Protocol types for duck-typed interfaces
|
|
48
|
+
# =====================================================================
|
|
49
|
+
|
|
50
|
+
@runtime_checkable
|
|
51
|
+
class SynthesisStepLike(Protocol):
|
|
52
|
+
"""Structural type for synthesis step objects.
|
|
53
|
+
|
|
54
|
+
Used by reports/ and process/ modules that accept step objects
|
|
55
|
+
with a .template attribute (e.g. SynthesisStep from synthesis_route).
|
|
56
|
+
"""
|
|
57
|
+
step_number: int
|
|
58
|
+
template: object # ReactionTemplate (avoid circular import)
|
|
59
|
+
precursors: list
|
|
60
|
+
product_smiles: str
|
|
61
|
+
product_name: str
|
|
62
|
+
expected_yield: float
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@runtime_checkable
|
|
66
|
+
class SynthesisRouteLike(Protocol):
|
|
67
|
+
"""Structural type for synthesis route objects.
|
|
68
|
+
|
|
69
|
+
Used by generate_synthesis_report() and process engineering modules.
|
|
70
|
+
"""
|
|
71
|
+
target_smiles: str
|
|
72
|
+
target_name: str
|
|
73
|
+
steps: list
|
|
74
|
+
overall_yield: float
|
|
75
|
+
starting_materials: list
|
|
76
|
+
total_steps: int
|
|
77
|
+
longest_linear_sequence: int
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@runtime_checkable
|
|
81
|
+
class CostBreakdownLike(Protocol):
|
|
82
|
+
"""Structural type for the cost breakdown sub-object."""
|
|
83
|
+
raw_materials_usd: float
|
|
84
|
+
labor_usd: float
|
|
85
|
+
equipment_usd: float
|
|
86
|
+
energy_usd: float
|
|
87
|
+
waste_disposal_usd: float
|
|
88
|
+
overhead_usd: float
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@runtime_checkable
|
|
92
|
+
class CostEstimateLike(Protocol):
|
|
93
|
+
"""Structural type for cost estimate objects.
|
|
94
|
+
|
|
95
|
+
Used by generate_cost_report().
|
|
96
|
+
"""
|
|
97
|
+
total_usd: float
|
|
98
|
+
per_kg_usd: float
|
|
99
|
+
scale_kg: float
|
|
100
|
+
breakdown: CostBreakdownLike
|
|
101
|
+
notes: list
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@runtime_checkable
|
|
105
|
+
class SafetyAssessmentLike(Protocol):
|
|
106
|
+
"""Structural type for per-step safety assessment objects.
|
|
107
|
+
|
|
108
|
+
Used by generate_safety_report().
|
|
109
|
+
"""
|
|
110
|
+
step_number: int
|
|
111
|
+
step_name: str
|
|
112
|
+
hazards: list
|
|
113
|
+
ppe_required: list
|
|
114
|
+
engineering_controls: list
|
|
115
|
+
emergency_procedures: list
|
|
116
|
+
incompatible_materials: list
|
|
117
|
+
waste_classification: str
|
|
118
|
+
risk_level: str
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@runtime_checkable
|
|
122
|
+
class MoleculeLike(Protocol):
|
|
123
|
+
"""Structural type for molecule-like objects.
|
|
124
|
+
|
|
125
|
+
Used by generate_molecule_report() and functional group detectors.
|
|
126
|
+
"""
|
|
127
|
+
name: str
|
|
128
|
+
atoms: list
|
|
129
|
+
bonds: list
|
|
130
|
+
|
|
131
|
+
def neighbors(self, idx: int) -> list[int]: ...
|
|
132
|
+
def get_bond(self, i: int, j: int) -> object | None: ...
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
__all__ = [
|
|
136
|
+
# Protocols
|
|
137
|
+
"SynthesisStepLike",
|
|
138
|
+
"SynthesisRouteLike",
|
|
139
|
+
"CostBreakdownLike",
|
|
140
|
+
"CostEstimateLike",
|
|
141
|
+
"SafetyAssessmentLike",
|
|
142
|
+
"MoleculeLike",
|
|
143
|
+
# Formatters
|
|
144
|
+
"section_header",
|
|
145
|
+
"subsection_header",
|
|
146
|
+
"ascii_table",
|
|
147
|
+
"word_wrap",
|
|
148
|
+
"bullet_list",
|
|
149
|
+
"key_value_block",
|
|
150
|
+
"horizontal_bar",
|
|
151
|
+
"format_currency",
|
|
152
|
+
"format_percent",
|
|
153
|
+
# Report generators
|
|
154
|
+
"generate_molecule_report",
|
|
155
|
+
"generate_synthesis_report",
|
|
156
|
+
"generate_safety_report",
|
|
157
|
+
"generate_cost_report",
|
|
158
|
+
]
|