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.
Files changed (78) hide show
  1. molbuilder/__init__.py +8 -0
  2. molbuilder/__main__.py +6 -0
  3. molbuilder/atomic/__init__.py +4 -0
  4. molbuilder/atomic/bohr.py +235 -0
  5. molbuilder/atomic/quantum_atom.py +334 -0
  6. molbuilder/atomic/quantum_numbers.py +196 -0
  7. molbuilder/atomic/wavefunctions.py +297 -0
  8. molbuilder/bonding/__init__.py +4 -0
  9. molbuilder/bonding/covalent.py +442 -0
  10. molbuilder/bonding/lewis.py +347 -0
  11. molbuilder/bonding/vsepr.py +433 -0
  12. molbuilder/cli/__init__.py +1 -0
  13. molbuilder/cli/demos.py +516 -0
  14. molbuilder/cli/menu.py +127 -0
  15. molbuilder/cli/wizard.py +831 -0
  16. molbuilder/core/__init__.py +6 -0
  17. molbuilder/core/bond_data.py +170 -0
  18. molbuilder/core/constants.py +51 -0
  19. molbuilder/core/element_properties.py +183 -0
  20. molbuilder/core/elements.py +181 -0
  21. molbuilder/core/geometry.py +232 -0
  22. molbuilder/gui/__init__.py +2 -0
  23. molbuilder/gui/app.py +286 -0
  24. molbuilder/gui/canvas3d.py +115 -0
  25. molbuilder/gui/dialogs.py +117 -0
  26. molbuilder/gui/event_handler.py +118 -0
  27. molbuilder/gui/sidebar.py +105 -0
  28. molbuilder/gui/toolbar.py +71 -0
  29. molbuilder/io/__init__.py +1 -0
  30. molbuilder/io/json_io.py +146 -0
  31. molbuilder/io/mol_sdf.py +169 -0
  32. molbuilder/io/pdb.py +184 -0
  33. molbuilder/io/smiles_io.py +47 -0
  34. molbuilder/io/xyz.py +103 -0
  35. molbuilder/molecule/__init__.py +2 -0
  36. molbuilder/molecule/amino_acids.py +919 -0
  37. molbuilder/molecule/builders.py +257 -0
  38. molbuilder/molecule/conformations.py +70 -0
  39. molbuilder/molecule/functional_groups.py +484 -0
  40. molbuilder/molecule/graph.py +712 -0
  41. molbuilder/molecule/peptides.py +13 -0
  42. molbuilder/molecule/stereochemistry.py +6 -0
  43. molbuilder/process/__init__.py +3 -0
  44. molbuilder/process/conditions.py +260 -0
  45. molbuilder/process/costing.py +316 -0
  46. molbuilder/process/purification.py +285 -0
  47. molbuilder/process/reactor.py +297 -0
  48. molbuilder/process/safety.py +476 -0
  49. molbuilder/process/scale_up.py +427 -0
  50. molbuilder/process/solvent_systems.py +204 -0
  51. molbuilder/reactions/__init__.py +3 -0
  52. molbuilder/reactions/functional_group_detect.py +728 -0
  53. molbuilder/reactions/knowledge_base.py +1716 -0
  54. molbuilder/reactions/reaction_types.py +102 -0
  55. molbuilder/reactions/reagent_data.py +1248 -0
  56. molbuilder/reactions/retrosynthesis.py +1430 -0
  57. molbuilder/reactions/synthesis_route.py +377 -0
  58. molbuilder/reports/__init__.py +158 -0
  59. molbuilder/reports/cost_report.py +206 -0
  60. molbuilder/reports/molecule_report.py +279 -0
  61. molbuilder/reports/safety_report.py +296 -0
  62. molbuilder/reports/synthesis_report.py +283 -0
  63. molbuilder/reports/text_formatter.py +170 -0
  64. molbuilder/smiles/__init__.py +4 -0
  65. molbuilder/smiles/parser.py +487 -0
  66. molbuilder/smiles/tokenizer.py +291 -0
  67. molbuilder/smiles/writer.py +375 -0
  68. molbuilder/visualization/__init__.py +1 -0
  69. molbuilder/visualization/bohr_viz.py +166 -0
  70. molbuilder/visualization/molecule_viz.py +368 -0
  71. molbuilder/visualization/quantum_viz.py +434 -0
  72. molbuilder/visualization/theme.py +12 -0
  73. molbuilder-1.0.0.dist-info/METADATA +360 -0
  74. molbuilder-1.0.0.dist-info/RECORD +78 -0
  75. molbuilder-1.0.0.dist-info/WHEEL +5 -0
  76. molbuilder-1.0.0.dist-info/entry_points.txt +2 -0
  77. molbuilder-1.0.0.dist-info/licenses/LICENSE +21 -0
  78. 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
+ ]