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,296 @@
|
|
|
1
|
+
"""Safety assessment report generator.
|
|
2
|
+
|
|
3
|
+
Produces a detailed ASCII safety report covering hazards, PPE
|
|
4
|
+
requirements, engineering controls, waste handling, and emergency
|
|
5
|
+
procedures for every step of a synthesis route.
|
|
6
|
+
All output is cp1252-safe.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Iterable
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from molbuilder.reports.text_formatter import (
|
|
15
|
+
section_header,
|
|
16
|
+
subsection_header,
|
|
17
|
+
ascii_table,
|
|
18
|
+
bullet_list,
|
|
19
|
+
key_value_block,
|
|
20
|
+
word_wrap,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from molbuilder.reports import SafetyAssessmentLike
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# =====================================================================
|
|
28
|
+
# Helpers
|
|
29
|
+
# =====================================================================
|
|
30
|
+
|
|
31
|
+
# Emergency contact numbers -- override these for non-US locales
|
|
32
|
+
EMERGENCY_NUMBER = "911"
|
|
33
|
+
POISON_CONTROL_NUMBER = "1-800-222-1222"
|
|
34
|
+
|
|
35
|
+
_RISK_ORDER = {
|
|
36
|
+
"low": 0,
|
|
37
|
+
"moderate": 1,
|
|
38
|
+
"high": 2,
|
|
39
|
+
"very high": 3,
|
|
40
|
+
"extreme": 4,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _risk_rank(level: str) -> int:
|
|
45
|
+
"""Numeric rank for a risk-level string (higher = worse)."""
|
|
46
|
+
return _RISK_ORDER.get(str(level).strip().lower(), -1)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _safe_list(obj, attr: str) -> list:
|
|
50
|
+
"""Return getattr(obj, attr) as a list, or [] on failure."""
|
|
51
|
+
val = getattr(obj, attr, None)
|
|
52
|
+
if val is None:
|
|
53
|
+
return []
|
|
54
|
+
if isinstance(val, (list, tuple)):
|
|
55
|
+
return list(val)
|
|
56
|
+
return [val]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _safe_str(value, default: str = "N/A") -> str:
|
|
60
|
+
if value is None:
|
|
61
|
+
return default
|
|
62
|
+
s = str(value)
|
|
63
|
+
return s if s else default
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# =====================================================================
|
|
67
|
+
# Public API
|
|
68
|
+
# =====================================================================
|
|
69
|
+
|
|
70
|
+
def generate_safety_report(
|
|
71
|
+
assessments: Iterable[SafetyAssessmentLike] | None,
|
|
72
|
+
) -> str:
|
|
73
|
+
"""Generate an ASCII safety report.
|
|
74
|
+
|
|
75
|
+
Uses duck typing -- *assessments* should be a list (or iterable)
|
|
76
|
+
where each item exposes:
|
|
77
|
+
|
|
78
|
+
* ``.step_number`` -- int
|
|
79
|
+
* ``.step_name`` -- str
|
|
80
|
+
* ``.hazards`` -- list of hazard objects, each with:
|
|
81
|
+
- ``.reagent_name`` -- str
|
|
82
|
+
- ``.ghs_hazards`` -- list[str] (e.g. ``["H225", "H301"]``)
|
|
83
|
+
- ``.hazard_descriptions`` -- list[str]
|
|
84
|
+
- ``.pictogram_descriptions`` -- list[str]
|
|
85
|
+
* ``.ppe_required`` -- list[str]
|
|
86
|
+
* ``.engineering_controls`` -- list[str]
|
|
87
|
+
* ``.emergency_procedures`` -- list[str]
|
|
88
|
+
* ``.incompatible_materials`` -- list[str]
|
|
89
|
+
* ``.waste_classification`` -- str
|
|
90
|
+
* ``.risk_level`` -- str (e.g. ``"low"``, ``"high"``)
|
|
91
|
+
"""
|
|
92
|
+
if assessments is not None:
|
|
93
|
+
for i, a in enumerate(assessments):
|
|
94
|
+
if not hasattr(a, 'step_number'):
|
|
95
|
+
raise TypeError(
|
|
96
|
+
f"Assessment {i} must have a 'step_number' attribute, "
|
|
97
|
+
f"got {type(a).__name__}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
lines: list[str] = []
|
|
101
|
+
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
# 1. Header
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
lines.append(section_header("Safety Assessment Report"))
|
|
106
|
+
lines.append("")
|
|
107
|
+
|
|
108
|
+
assessment_list = list(assessments) if assessments else []
|
|
109
|
+
if not assessment_list:
|
|
110
|
+
lines.append(" No safety assessments provided.")
|
|
111
|
+
lines.append("")
|
|
112
|
+
lines.append("=" * 70)
|
|
113
|
+
return "\n".join(lines)
|
|
114
|
+
|
|
115
|
+
# ------------------------------------------------------------------
|
|
116
|
+
# 2. Overall Risk Summary
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
lines.append(subsection_header("Overall Risk Summary"))
|
|
119
|
+
|
|
120
|
+
risk_levels: list[str] = []
|
|
121
|
+
for a in assessment_list:
|
|
122
|
+
rl = _safe_str(getattr(a, "risk_level", None), "unknown")
|
|
123
|
+
risk_levels.append(rl)
|
|
124
|
+
|
|
125
|
+
# Determine highest risk
|
|
126
|
+
highest_risk = "unknown"
|
|
127
|
+
highest_rank = -1
|
|
128
|
+
for rl in risk_levels:
|
|
129
|
+
rank = _risk_rank(rl)
|
|
130
|
+
if rank > highest_rank:
|
|
131
|
+
highest_rank = rank
|
|
132
|
+
highest_risk = rl
|
|
133
|
+
|
|
134
|
+
overview = [
|
|
135
|
+
("Total Steps Assessed", str(len(assessment_list))),
|
|
136
|
+
("Highest Risk Level", highest_risk.upper()),
|
|
137
|
+
]
|
|
138
|
+
lines.append(key_value_block(overview))
|
|
139
|
+
lines.append("")
|
|
140
|
+
|
|
141
|
+
# Per-step risk overview table
|
|
142
|
+
risk_headers = ["Step", "Name", "Risk Level"]
|
|
143
|
+
risk_rows: list[list[str]] = []
|
|
144
|
+
for a in assessment_list:
|
|
145
|
+
sn = _safe_str(getattr(a, "step_number", None), "?")
|
|
146
|
+
nm = _safe_str(getattr(a, "step_name", None), "Unnamed")
|
|
147
|
+
rl = _safe_str(getattr(a, "risk_level", None), "unknown")
|
|
148
|
+
risk_rows.append([str(sn), nm, rl.upper()])
|
|
149
|
+
lines.append(ascii_table(
|
|
150
|
+
risk_headers, risk_rows,
|
|
151
|
+
alignments=["r", "l", "c"],
|
|
152
|
+
min_widths=[5, 30, 14],
|
|
153
|
+
))
|
|
154
|
+
lines.append("")
|
|
155
|
+
|
|
156
|
+
# ------------------------------------------------------------------
|
|
157
|
+
# 3. Consolidated PPE Requirements
|
|
158
|
+
# ------------------------------------------------------------------
|
|
159
|
+
lines.append(subsection_header("PPE Requirements (All Steps)"))
|
|
160
|
+
all_ppe: set[str] = set()
|
|
161
|
+
for a in assessment_list:
|
|
162
|
+
for item in _safe_list(a, "ppe_required"):
|
|
163
|
+
all_ppe.add(str(item))
|
|
164
|
+
|
|
165
|
+
if all_ppe:
|
|
166
|
+
lines.append(bullet_list(sorted(all_ppe)))
|
|
167
|
+
else:
|
|
168
|
+
lines.append(" No specific PPE requirements listed.")
|
|
169
|
+
lines.append("")
|
|
170
|
+
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
# 4. Per-Step Hazard Details
|
|
173
|
+
# ------------------------------------------------------------------
|
|
174
|
+
lines.append(section_header("Per-Step Hazard Details"))
|
|
175
|
+
lines.append("")
|
|
176
|
+
|
|
177
|
+
for a in assessment_list:
|
|
178
|
+
step_num = _safe_str(getattr(a, "step_number", None), "?")
|
|
179
|
+
step_name = _safe_str(getattr(a, "step_name", None), "Unnamed")
|
|
180
|
+
risk_level = _safe_str(getattr(a, "risk_level", None), "unknown")
|
|
181
|
+
|
|
182
|
+
lines.append(subsection_header(
|
|
183
|
+
f"Step {step_num}: {step_name} [Risk: {risk_level.upper()}]"
|
|
184
|
+
))
|
|
185
|
+
|
|
186
|
+
# Reagent hazards
|
|
187
|
+
hazards = _safe_list(a, "hazards")
|
|
188
|
+
if hazards:
|
|
189
|
+
for h in hazards:
|
|
190
|
+
reagent_name = _safe_str(getattr(h, "reagent_name", None),
|
|
191
|
+
"Unknown reagent")
|
|
192
|
+
lines.append(f" Reagent: {reagent_name}")
|
|
193
|
+
|
|
194
|
+
ghs = _safe_list(h, "ghs_hazards")
|
|
195
|
+
if ghs:
|
|
196
|
+
lines.append(f" GHS Codes: {', '.join(str(c) for c in ghs)}")
|
|
197
|
+
|
|
198
|
+
descs = _safe_list(h, "hazard_descriptions")
|
|
199
|
+
if descs:
|
|
200
|
+
lines.append(" Hazard Descriptions:")
|
|
201
|
+
lines.append(bullet_list(
|
|
202
|
+
[str(d) for d in descs], indent=6))
|
|
203
|
+
|
|
204
|
+
pictos = _safe_list(h, "pictogram_descriptions")
|
|
205
|
+
if pictos:
|
|
206
|
+
lines.append(" Pictograms:")
|
|
207
|
+
lines.append(bullet_list(
|
|
208
|
+
[str(p) for p in pictos], indent=6))
|
|
209
|
+
lines.append("")
|
|
210
|
+
else:
|
|
211
|
+
lines.append(" No reagent hazards listed.")
|
|
212
|
+
lines.append("")
|
|
213
|
+
|
|
214
|
+
# Engineering controls
|
|
215
|
+
controls = _safe_list(a, "engineering_controls")
|
|
216
|
+
if controls:
|
|
217
|
+
lines.append(" Engineering Controls:")
|
|
218
|
+
lines.append(bullet_list([str(c) for c in controls], indent=4))
|
|
219
|
+
lines.append("")
|
|
220
|
+
|
|
221
|
+
# Incompatible materials
|
|
222
|
+
incompat = _safe_list(a, "incompatible_materials")
|
|
223
|
+
if incompat:
|
|
224
|
+
lines.append(" Incompatible Materials:")
|
|
225
|
+
lines.append(bullet_list([str(m) for m in incompat], indent=4))
|
|
226
|
+
lines.append("")
|
|
227
|
+
|
|
228
|
+
# ------------------------------------------------------------------
|
|
229
|
+
# 5. Waste Handling
|
|
230
|
+
# ------------------------------------------------------------------
|
|
231
|
+
lines.append(section_header("Waste Handling"))
|
|
232
|
+
lines.append("")
|
|
233
|
+
|
|
234
|
+
waste_headers = ["Step", "Name", "Waste Classification"]
|
|
235
|
+
waste_rows: list[list[str]] = []
|
|
236
|
+
for a in assessment_list:
|
|
237
|
+
sn = _safe_str(getattr(a, "step_number", None), "?")
|
|
238
|
+
nm = _safe_str(getattr(a, "step_name", None), "Unnamed")
|
|
239
|
+
wc = _safe_str(getattr(a, "waste_classification", None), "Unclassified")
|
|
240
|
+
waste_rows.append([str(sn), nm, wc])
|
|
241
|
+
|
|
242
|
+
lines.append(ascii_table(
|
|
243
|
+
waste_headers, waste_rows,
|
|
244
|
+
alignments=["r", "l", "l"],
|
|
245
|
+
min_widths=[5, 25, 25],
|
|
246
|
+
))
|
|
247
|
+
lines.append("")
|
|
248
|
+
|
|
249
|
+
lines.append(" General Waste Disposal Guidance:")
|
|
250
|
+
disposal_notes = [
|
|
251
|
+
"Segregate waste by classification (halogenated, aqueous, "
|
|
252
|
+
"heavy-metal, etc.)",
|
|
253
|
+
"Label all waste containers with contents, hazard class, "
|
|
254
|
+
"and date",
|
|
255
|
+
"Never mix incompatible waste streams",
|
|
256
|
+
"Contact EHS for disposal of unknown or high-hazard waste",
|
|
257
|
+
]
|
|
258
|
+
lines.append(bullet_list(disposal_notes, indent=4))
|
|
259
|
+
lines.append("")
|
|
260
|
+
|
|
261
|
+
# ------------------------------------------------------------------
|
|
262
|
+
# 6. Emergency Procedures
|
|
263
|
+
# ------------------------------------------------------------------
|
|
264
|
+
lines.append(section_header("Emergency Procedures"))
|
|
265
|
+
lines.append("")
|
|
266
|
+
|
|
267
|
+
all_emergency: list[str] = []
|
|
268
|
+
for a in assessment_list:
|
|
269
|
+
step_num = _safe_str(getattr(a, "step_number", None), "?")
|
|
270
|
+
step_name = _safe_str(getattr(a, "step_name", None), "Unnamed")
|
|
271
|
+
procs = _safe_list(a, "emergency_procedures")
|
|
272
|
+
if procs:
|
|
273
|
+
lines.append(subsection_header(f"Step {step_num}: {step_name}"))
|
|
274
|
+
lines.append(bullet_list([str(p) for p in procs], indent=4))
|
|
275
|
+
lines.append("")
|
|
276
|
+
all_emergency.extend(str(p) for p in procs)
|
|
277
|
+
|
|
278
|
+
if not all_emergency:
|
|
279
|
+
lines.append(" No step-specific emergency procedures listed.")
|
|
280
|
+
lines.append("")
|
|
281
|
+
|
|
282
|
+
lines.append(subsection_header("General Emergency Contacts"))
|
|
283
|
+
contacts = [
|
|
284
|
+
("Emergency Services", EMERGENCY_NUMBER),
|
|
285
|
+
("Poison Control", POISON_CONTROL_NUMBER),
|
|
286
|
+
("Campus/Site EHS", "See posted numbers in laboratory"),
|
|
287
|
+
]
|
|
288
|
+
lines.append(key_value_block(contacts, indent=4))
|
|
289
|
+
lines.append("")
|
|
290
|
+
|
|
291
|
+
# Footer
|
|
292
|
+
lines.append("=" * 70)
|
|
293
|
+
lines.append(" End of Safety Assessment Report")
|
|
294
|
+
lines.append("=" * 70)
|
|
295
|
+
|
|
296
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Synthesis route report generator.
|
|
2
|
+
|
|
3
|
+
Produces a detailed ASCII report of a multi-step synthesis route
|
|
4
|
+
including reagents, conditions, yields, and starting materials.
|
|
5
|
+
All output is cp1252-safe.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from molbuilder.reports.text_formatter import (
|
|
13
|
+
section_header,
|
|
14
|
+
subsection_header,
|
|
15
|
+
ascii_table,
|
|
16
|
+
bullet_list,
|
|
17
|
+
key_value_block,
|
|
18
|
+
word_wrap,
|
|
19
|
+
format_percent,
|
|
20
|
+
format_currency,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from molbuilder.reports import SynthesisRouteLike
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# =====================================================================
|
|
28
|
+
# Helpers
|
|
29
|
+
# =====================================================================
|
|
30
|
+
|
|
31
|
+
def _safe_str(value, default: str = "N/A") -> str:
|
|
32
|
+
"""Convert *value* to a string, returning *default* if None or empty."""
|
|
33
|
+
if value is None:
|
|
34
|
+
return default
|
|
35
|
+
s = str(value)
|
|
36
|
+
return s if s else default
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _yield_str(yld) -> str:
|
|
40
|
+
"""Format a yield value (float 0-100 or None) as a percentage."""
|
|
41
|
+
if yld is None:
|
|
42
|
+
return "N/A"
|
|
43
|
+
try:
|
|
44
|
+
return format_percent(float(yld))
|
|
45
|
+
except (TypeError, ValueError):
|
|
46
|
+
return _safe_str(yld)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _temp_str(template) -> str:
|
|
50
|
+
"""Extract a human-readable temperature string from a template."""
|
|
51
|
+
try:
|
|
52
|
+
lo, hi = template.temperature_range
|
|
53
|
+
if lo == hi:
|
|
54
|
+
return f"{lo:.0f} C"
|
|
55
|
+
return f"{lo:.0f} - {hi:.0f} C"
|
|
56
|
+
except Exception:
|
|
57
|
+
return "N/A"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# =====================================================================
|
|
61
|
+
# Public API
|
|
62
|
+
# =====================================================================
|
|
63
|
+
|
|
64
|
+
def generate_synthesis_report(route: SynthesisRouteLike) -> str:
|
|
65
|
+
"""Generate an ASCII synthesis route report.
|
|
66
|
+
|
|
67
|
+
Uses duck typing -- *route* should expose:
|
|
68
|
+
|
|
69
|
+
* ``.target_smiles`` -- SMILES of the target molecule
|
|
70
|
+
* ``.target_name`` -- human-readable target name
|
|
71
|
+
* ``.steps`` -- list of step objects (see below)
|
|
72
|
+
* ``.overall_yield`` -- float (percent)
|
|
73
|
+
* ``.starting_materials`` -- list of material objects with ``.name``, ``.smiles``
|
|
74
|
+
* ``.total_steps`` -- int
|
|
75
|
+
* ``.longest_linear_sequence`` -- int
|
|
76
|
+
|
|
77
|
+
Each **step** should expose:
|
|
78
|
+
|
|
79
|
+
* ``.step_number`` -- int
|
|
80
|
+
* ``.template`` -- ReactionTemplate (has ``.name``, ``.category``,
|
|
81
|
+
``.reagents``, ``.solvents``, ``.catalysts``, ``.temperature_range``,
|
|
82
|
+
``.typical_yield``, ``.mechanism``, ``.safety_notes``)
|
|
83
|
+
* ``.precursors`` -- list with ``.name``, ``.smiles``
|
|
84
|
+
* ``.product_smiles`` -- str
|
|
85
|
+
* ``.product_name`` -- str
|
|
86
|
+
* ``.conditions`` -- str or dict
|
|
87
|
+
* ``.expected_yield`` -- float (percent)
|
|
88
|
+
* ``.notes`` -- str
|
|
89
|
+
"""
|
|
90
|
+
if not hasattr(route, 'steps'):
|
|
91
|
+
raise TypeError(
|
|
92
|
+
f"route must have a 'steps' attribute, "
|
|
93
|
+
f"got {type(route).__name__}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
lines: list[str] = []
|
|
97
|
+
|
|
98
|
+
# ------------------------------------------------------------------
|
|
99
|
+
# 1. Header
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
target_name = _safe_str(getattr(route, "target_name", None), "Target")
|
|
102
|
+
lines.append(section_header(f"Synthesis Route: {target_name}"))
|
|
103
|
+
lines.append("")
|
|
104
|
+
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
# 2. Route Overview
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
lines.append(subsection_header("Route Overview"))
|
|
109
|
+
|
|
110
|
+
target_smiles = _safe_str(getattr(route, "target_smiles", None))
|
|
111
|
+
total_steps = getattr(route, "total_steps", None)
|
|
112
|
+
lls = getattr(route, "longest_linear_sequence", None)
|
|
113
|
+
overall_yield = getattr(route, "overall_yield", None)
|
|
114
|
+
|
|
115
|
+
overview = [
|
|
116
|
+
("Target", target_name),
|
|
117
|
+
("Target SMILES", target_smiles),
|
|
118
|
+
("Total Steps", _safe_str(total_steps)),
|
|
119
|
+
("Longest Linear Sequence", _safe_str(lls)),
|
|
120
|
+
("Overall Yield", _yield_str(overall_yield)),
|
|
121
|
+
]
|
|
122
|
+
lines.append(key_value_block(overview))
|
|
123
|
+
lines.append("")
|
|
124
|
+
|
|
125
|
+
# Starting materials list
|
|
126
|
+
starting_materials = getattr(route, "starting_materials", []) or []
|
|
127
|
+
if starting_materials:
|
|
128
|
+
lines.append(subsection_header("Starting Materials"))
|
|
129
|
+
sm_headers = ["#", "Name", "SMILES"]
|
|
130
|
+
sm_rows: list[list[str]] = []
|
|
131
|
+
for i, sm in enumerate(starting_materials, 1):
|
|
132
|
+
sm_name = _safe_str(getattr(sm, "name", None), f"Material {i}")
|
|
133
|
+
sm_smiles = _safe_str(getattr(sm, "smiles", None))
|
|
134
|
+
sm_rows.append([str(i), sm_name, sm_smiles])
|
|
135
|
+
lines.append(ascii_table(
|
|
136
|
+
sm_headers, sm_rows,
|
|
137
|
+
alignments=["r", "l", "l"],
|
|
138
|
+
min_widths=[3, 25, 25],
|
|
139
|
+
))
|
|
140
|
+
lines.append("")
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
# 3. Step-by-step Details
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
steps = getattr(route, "steps", []) or []
|
|
146
|
+
if steps:
|
|
147
|
+
lines.append(section_header("Step-by-Step Details"))
|
|
148
|
+
lines.append("")
|
|
149
|
+
|
|
150
|
+
for step in steps:
|
|
151
|
+
step_num = getattr(step, "step_number", "?")
|
|
152
|
+
product_name = _safe_str(getattr(step, "product_name", None))
|
|
153
|
+
product_smiles = _safe_str(getattr(step, "product_smiles", None))
|
|
154
|
+
|
|
155
|
+
lines.append(subsection_header(f"Step {step_num}: {product_name}"))
|
|
156
|
+
|
|
157
|
+
# Template info
|
|
158
|
+
template = getattr(step, "template", None)
|
|
159
|
+
if template is not None:
|
|
160
|
+
rxn_name = _safe_str(getattr(template, "name", None), "Unknown reaction")
|
|
161
|
+
named_rxn = getattr(template, "named_reaction", None)
|
|
162
|
+
category = getattr(template, "category", None)
|
|
163
|
+
cat_str = category.name if category is not None else "N/A"
|
|
164
|
+
|
|
165
|
+
step_info = [
|
|
166
|
+
("Reaction", rxn_name),
|
|
167
|
+
]
|
|
168
|
+
if named_rxn:
|
|
169
|
+
step_info.append(("Named Reaction", str(named_rxn)))
|
|
170
|
+
step_info.append(("Category", cat_str))
|
|
171
|
+
step_info.append(("Product SMILES", product_smiles))
|
|
172
|
+
lines.append(key_value_block(step_info))
|
|
173
|
+
lines.append("")
|
|
174
|
+
|
|
175
|
+
# Reagents
|
|
176
|
+
reagents = getattr(template, "reagents", []) or []
|
|
177
|
+
if reagents:
|
|
178
|
+
lines.append(" Reagents:")
|
|
179
|
+
lines.append(bullet_list(reagents, indent=4))
|
|
180
|
+
lines.append("")
|
|
181
|
+
|
|
182
|
+
# Solvents
|
|
183
|
+
solvents = getattr(template, "solvents", []) or []
|
|
184
|
+
if solvents:
|
|
185
|
+
lines.append(" Solvents:")
|
|
186
|
+
lines.append(bullet_list(solvents, indent=4))
|
|
187
|
+
lines.append("")
|
|
188
|
+
|
|
189
|
+
# Catalysts
|
|
190
|
+
catalysts = getattr(template, "catalysts", []) or []
|
|
191
|
+
if catalysts:
|
|
192
|
+
lines.append(" Catalysts:")
|
|
193
|
+
lines.append(bullet_list(catalysts, indent=4))
|
|
194
|
+
lines.append("")
|
|
195
|
+
|
|
196
|
+
# Temperature
|
|
197
|
+
lines.append(f" Temperature: {_temp_str(template)}")
|
|
198
|
+
|
|
199
|
+
# Typical yield range from template
|
|
200
|
+
try:
|
|
201
|
+
lo, hi = template.typical_yield
|
|
202
|
+
lines.append(f" Typical Yield Range: {lo:.0f}% - {hi:.0f}%")
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
else:
|
|
206
|
+
lines.append(key_value_block([
|
|
207
|
+
("Product SMILES", product_smiles),
|
|
208
|
+
]))
|
|
209
|
+
lines.append("")
|
|
210
|
+
|
|
211
|
+
# Expected yield for this specific step
|
|
212
|
+
expected_yield = getattr(step, "expected_yield", None)
|
|
213
|
+
lines.append(f" Expected Yield: {_yield_str(expected_yield)}")
|
|
214
|
+
|
|
215
|
+
# Precursors
|
|
216
|
+
precursors = getattr(step, "precursors", []) or []
|
|
217
|
+
if precursors:
|
|
218
|
+
lines.append("")
|
|
219
|
+
lines.append(" Precursors:")
|
|
220
|
+
for p in precursors:
|
|
221
|
+
p_name = _safe_str(getattr(p, "name", None), "Unknown")
|
|
222
|
+
p_smiles = _safe_str(getattr(p, "smiles", None))
|
|
223
|
+
lines.append(f" - {p_name} ({p_smiles})")
|
|
224
|
+
|
|
225
|
+
# Conditions (may be a string or dict)
|
|
226
|
+
conditions = getattr(step, "conditions", None)
|
|
227
|
+
if conditions:
|
|
228
|
+
lines.append("")
|
|
229
|
+
if isinstance(conditions, dict):
|
|
230
|
+
cond_pairs = [(str(k), str(v)) for k, v in conditions.items()]
|
|
231
|
+
lines.append(" Conditions:")
|
|
232
|
+
lines.append(key_value_block(cond_pairs, indent=4))
|
|
233
|
+
else:
|
|
234
|
+
lines.append(word_wrap(f" Conditions: {conditions}", indent=2))
|
|
235
|
+
|
|
236
|
+
# Mechanism description from template
|
|
237
|
+
if template is not None:
|
|
238
|
+
mechanism = getattr(template, "mechanism", "")
|
|
239
|
+
if mechanism:
|
|
240
|
+
lines.append("")
|
|
241
|
+
lines.append(" Mechanism:")
|
|
242
|
+
lines.append(word_wrap(mechanism, indent=4))
|
|
243
|
+
|
|
244
|
+
safety = getattr(template, "safety_notes", "")
|
|
245
|
+
if safety:
|
|
246
|
+
lines.append("")
|
|
247
|
+
lines.append(" Safety Notes:")
|
|
248
|
+
lines.append(word_wrap(safety, indent=4))
|
|
249
|
+
|
|
250
|
+
# Step notes
|
|
251
|
+
notes = getattr(step, "notes", "")
|
|
252
|
+
if notes:
|
|
253
|
+
lines.append("")
|
|
254
|
+
lines.append(" Notes:")
|
|
255
|
+
lines.append(word_wrap(str(notes), indent=4))
|
|
256
|
+
|
|
257
|
+
lines.append("")
|
|
258
|
+
|
|
259
|
+
# ------------------------------------------------------------------
|
|
260
|
+
# 4. Starting Materials Summary Table
|
|
261
|
+
# ------------------------------------------------------------------
|
|
262
|
+
if starting_materials:
|
|
263
|
+
lines.append(section_header("Starting Materials Summary"))
|
|
264
|
+
lines.append("")
|
|
265
|
+
sum_headers = ["Material", "SMILES"]
|
|
266
|
+
sum_rows: list[list[str]] = []
|
|
267
|
+
for sm in starting_materials:
|
|
268
|
+
sm_name = _safe_str(getattr(sm, "name", None), "Unknown")
|
|
269
|
+
sm_smiles = _safe_str(getattr(sm, "smiles", None))
|
|
270
|
+
sum_rows.append([sm_name, sm_smiles])
|
|
271
|
+
lines.append(ascii_table(
|
|
272
|
+
sum_headers, sum_rows,
|
|
273
|
+
alignments=["l", "l"],
|
|
274
|
+
min_widths=[30, 30],
|
|
275
|
+
))
|
|
276
|
+
lines.append("")
|
|
277
|
+
|
|
278
|
+
# Footer
|
|
279
|
+
lines.append("=" * 70)
|
|
280
|
+
lines.append(" End of Synthesis Report")
|
|
281
|
+
lines.append("=" * 70)
|
|
282
|
+
|
|
283
|
+
return "\n".join(lines)
|