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,476 @@
|
|
|
1
|
+
"""Safety assessment for synthesis routes.
|
|
2
|
+
|
|
3
|
+
Performs GHS hazard lookup for every reagent in every step, determines
|
|
4
|
+
PPE requirements, engineering controls, emergency procedures, and
|
|
5
|
+
produces a per-step :class:`SafetyAssessment`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, List
|
|
12
|
+
|
|
13
|
+
from molbuilder.reactions.reaction_types import ReactionCategory, ReactionTemplate
|
|
14
|
+
from molbuilder.reactions.reagent_data import REAGENT_DB, get_reagent, Reagent, normalize_reagent_name
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# =====================================================================
|
|
18
|
+
# GHS reference tables
|
|
19
|
+
# =====================================================================
|
|
20
|
+
|
|
21
|
+
GHS_PICTOGRAMS: dict[str, str] = {
|
|
22
|
+
"GHS01": "Exploding bomb -- Explosives, self-reactive, organic peroxides",
|
|
23
|
+
"GHS02": "Flame -- Flammable gases/liquids/solids, pyrophoric, self-heating, "
|
|
24
|
+
"emits flammable gas on water contact",
|
|
25
|
+
"GHS03": "Flame over circle -- Oxidizers",
|
|
26
|
+
"GHS04": "Gas cylinder -- Compressed, liquefied, or dissolved gases",
|
|
27
|
+
"GHS05": "Corrosion -- Corrosive to metals, skin corrosion, serious eye damage",
|
|
28
|
+
"GHS06": "Skull and crossbones -- Acute toxicity (fatal or toxic)",
|
|
29
|
+
"GHS07": "Exclamation mark -- Irritant, narcotic, acute toxicity (harmful), "
|
|
30
|
+
"skin sensitizer",
|
|
31
|
+
"GHS08": "Health hazard -- Carcinogenicity, mutagenicity, reproductive toxicity, "
|
|
32
|
+
"respiratory sensitizer, organ toxicity, aspiration hazard",
|
|
33
|
+
"GHS09": "Environment -- Aquatic toxicity",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
GHS_HAZARD_STATEMENTS: dict[str, str] = {
|
|
37
|
+
# Physical hazards
|
|
38
|
+
"H200": "Unstable explosive",
|
|
39
|
+
"H201": "Explosive; mass explosion hazard",
|
|
40
|
+
"H202": "Explosive; severe projection hazard",
|
|
41
|
+
"H203": "Explosive; fire, blast or projection hazard",
|
|
42
|
+
"H204": "Fire or projection hazard",
|
|
43
|
+
"H205": "May mass explode in fire",
|
|
44
|
+
"H220": "Extremely flammable gas",
|
|
45
|
+
"H221": "Flammable gas",
|
|
46
|
+
"H222": "Extremely flammable aerosol",
|
|
47
|
+
"H223": "Flammable aerosol",
|
|
48
|
+
"H224": "Extremely flammable liquid and vapour",
|
|
49
|
+
"H225": "Highly flammable liquid and vapour",
|
|
50
|
+
"H226": "Flammable liquid and vapour",
|
|
51
|
+
"H227": "Combustible liquid",
|
|
52
|
+
"H228": "Flammable solid",
|
|
53
|
+
"H240": "Heating may cause an explosion",
|
|
54
|
+
"H241": "Heating may cause a fire or explosion",
|
|
55
|
+
"H242": "Heating may cause a fire",
|
|
56
|
+
"H250": "Catches fire spontaneously if exposed to air",
|
|
57
|
+
"H251": "Self-heating; may catch fire",
|
|
58
|
+
"H252": "Self-heating in large quantities; may catch fire",
|
|
59
|
+
"H260": "In contact with water releases flammable gases which may ignite spontaneously",
|
|
60
|
+
"H261": "In contact with water releases flammable gas",
|
|
61
|
+
"H270": "May cause or intensify fire; oxidizer",
|
|
62
|
+
"H271": "May cause fire or explosion; strong oxidizer",
|
|
63
|
+
"H272": "May intensify fire; oxidizer",
|
|
64
|
+
"H280": "Contains gas under pressure; may explode if heated",
|
|
65
|
+
"H281": "Contains refrigerated gas; may cause cryogenic burns or injury",
|
|
66
|
+
"H290": "May be corrosive to metals",
|
|
67
|
+
# Health hazards
|
|
68
|
+
"H300": "Fatal if swallowed",
|
|
69
|
+
"H301": "Toxic if swallowed",
|
|
70
|
+
"H302": "Harmful if swallowed",
|
|
71
|
+
"H304": "May be fatal if swallowed and enters airways",
|
|
72
|
+
"H305": "May be harmful if swallowed and enters airways",
|
|
73
|
+
"H310": "Fatal in contact with skin",
|
|
74
|
+
"H311": "Toxic in contact with skin",
|
|
75
|
+
"H312": "Harmful in contact with skin",
|
|
76
|
+
"H314": "Causes severe skin burns and eye damage",
|
|
77
|
+
"H315": "Causes skin irritation",
|
|
78
|
+
"H317": "May cause an allergic skin reaction",
|
|
79
|
+
"H318": "Causes serious eye damage",
|
|
80
|
+
"H319": "Causes serious eye irritation",
|
|
81
|
+
"H330": "Fatal if inhaled",
|
|
82
|
+
"H331": "Toxic if inhaled",
|
|
83
|
+
"H332": "Harmful if inhaled",
|
|
84
|
+
"H334": "May cause allergy or asthma symptoms or breathing difficulties if inhaled",
|
|
85
|
+
"H335": "May cause respiratory irritation",
|
|
86
|
+
"H336": "May cause drowsiness or dizziness",
|
|
87
|
+
"H340": "May cause genetic defects",
|
|
88
|
+
"H341": "Suspected of causing genetic defects",
|
|
89
|
+
"H350": "May cause cancer",
|
|
90
|
+
"H351": "Suspected of causing cancer",
|
|
91
|
+
"H360": "May damage fertility or the unborn child",
|
|
92
|
+
"H361": "Suspected of damaging fertility or the unborn child",
|
|
93
|
+
"H362": "May cause harm to breast-fed children",
|
|
94
|
+
"H360F": "May damage fertility",
|
|
95
|
+
"H360D": "May damage the unborn child",
|
|
96
|
+
"H360Fd": "May damage fertility; suspected of damaging the unborn child",
|
|
97
|
+
"H360Df": "May damage the unborn child; suspected of damaging fertility",
|
|
98
|
+
"H370": "Causes damage to organs",
|
|
99
|
+
"H371": "May cause damage to organs",
|
|
100
|
+
"H372": "Causes damage to organs through prolonged or repeated exposure",
|
|
101
|
+
"H373": "May cause damage to organs through prolonged or repeated exposure",
|
|
102
|
+
# Environmental hazards
|
|
103
|
+
"H400": "Very toxic to aquatic life",
|
|
104
|
+
"H410": "Very toxic to aquatic life with long lasting effects",
|
|
105
|
+
"H411": "Toxic to aquatic life with long lasting effects",
|
|
106
|
+
"H412": "Harmful to aquatic life with long lasting effects",
|
|
107
|
+
"H413": "May cause long lasting harmful effects to aquatic life",
|
|
108
|
+
"H420": "Harms public health and the environment by destroying ozone in the upper atmosphere",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# =====================================================================
|
|
113
|
+
# Data classes
|
|
114
|
+
# =====================================================================
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class HazardInfo:
|
|
118
|
+
"""GHS hazard information for a single reagent."""
|
|
119
|
+
|
|
120
|
+
reagent_name: str
|
|
121
|
+
ghs_hazards: list[str]
|
|
122
|
+
ghs_pictograms: list[str]
|
|
123
|
+
hazard_descriptions: list[str]
|
|
124
|
+
pictogram_descriptions: list[str]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class SafetyAssessment:
|
|
129
|
+
"""Complete safety assessment for one synthesis step."""
|
|
130
|
+
|
|
131
|
+
step_number: int
|
|
132
|
+
step_name: str
|
|
133
|
+
hazards: list[HazardInfo]
|
|
134
|
+
ppe_required: list[str]
|
|
135
|
+
engineering_controls: list[str]
|
|
136
|
+
emergency_procedures: list[str]
|
|
137
|
+
incompatible_materials: list[str]
|
|
138
|
+
waste_classification: str
|
|
139
|
+
risk_level: str # "low", "medium", "high"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# =====================================================================
|
|
143
|
+
# Internal helpers
|
|
144
|
+
# =====================================================================
|
|
145
|
+
|
|
146
|
+
def _build_hazard_info(reagent_name: str) -> HazardInfo:
|
|
147
|
+
"""Look up a reagent and compile its hazard information."""
|
|
148
|
+
reagent = get_reagent(reagent_name)
|
|
149
|
+
if reagent is None:
|
|
150
|
+
return HazardInfo(
|
|
151
|
+
reagent_name=reagent_name,
|
|
152
|
+
ghs_hazards=[],
|
|
153
|
+
ghs_pictograms=[],
|
|
154
|
+
hazard_descriptions=["Hazard data not available in database"],
|
|
155
|
+
pictogram_descriptions=[],
|
|
156
|
+
)
|
|
157
|
+
haz_descs = [
|
|
158
|
+
GHS_HAZARD_STATEMENTS.get(h, f"Unknown hazard code {h}")
|
|
159
|
+
for h in reagent.ghs_hazards
|
|
160
|
+
]
|
|
161
|
+
pic_descs = [
|
|
162
|
+
GHS_PICTOGRAMS.get(p, f"Unknown pictogram {p}")
|
|
163
|
+
for p in reagent.ghs_pictograms
|
|
164
|
+
]
|
|
165
|
+
return HazardInfo(
|
|
166
|
+
reagent_name=reagent.name,
|
|
167
|
+
ghs_hazards=list(reagent.ghs_hazards),
|
|
168
|
+
ghs_pictograms=list(reagent.ghs_pictograms),
|
|
169
|
+
hazard_descriptions=haz_descs,
|
|
170
|
+
pictogram_descriptions=pic_descs,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _determine_ppe(all_hazards: set[str], all_pictograms: set[str]) -> list[str]:
|
|
175
|
+
"""Determine required PPE from the union of hazard codes."""
|
|
176
|
+
ppe: list[str] = []
|
|
177
|
+
# Always require baseline
|
|
178
|
+
ppe.append("Safety goggles (splash-proof)")
|
|
179
|
+
ppe.append("Lab coat")
|
|
180
|
+
ppe.append("Closed-toe shoes")
|
|
181
|
+
|
|
182
|
+
if "GHS05" in all_pictograms or "H314" in all_hazards:
|
|
183
|
+
ppe.append("Chemical-resistant gloves (e.g. butyl rubber or nitrile)")
|
|
184
|
+
ppe.append("Face shield")
|
|
185
|
+
ppe.append("Chemical-resistant apron")
|
|
186
|
+
else:
|
|
187
|
+
ppe.append("Nitrile gloves (double-gloving recommended)")
|
|
188
|
+
|
|
189
|
+
if any(h in all_hazards for h in ("H330", "H331", "H332", "H335")):
|
|
190
|
+
ppe.append("Respiratory protection (fume hood minimum; respirator if outside hood)")
|
|
191
|
+
|
|
192
|
+
if "GHS06" in all_pictograms or "H300" in all_hazards or "H310" in all_hazards:
|
|
193
|
+
ppe.append("Emergency eyewash and safety shower must be accessible within 10 seconds")
|
|
194
|
+
|
|
195
|
+
if "GHS02" in all_pictograms or "H250" in all_hazards or "H260" in all_hazards:
|
|
196
|
+
ppe.append("Fire-resistant lab coat or Nomex coveralls for pyrophoric work")
|
|
197
|
+
|
|
198
|
+
return ppe
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _determine_engineering_controls(
|
|
202
|
+
all_hazards: set[str],
|
|
203
|
+
all_pictograms: set[str],
|
|
204
|
+
template: ReactionTemplate,
|
|
205
|
+
) -> list[str]:
|
|
206
|
+
"""Determine engineering controls."""
|
|
207
|
+
controls: list[str] = []
|
|
208
|
+
|
|
209
|
+
# Fume hood is baseline for organic chemistry
|
|
210
|
+
controls.append("Perform all operations in a well-ventilated fume hood")
|
|
211
|
+
|
|
212
|
+
if "GHS02" in all_pictograms:
|
|
213
|
+
controls.append("Remove all ignition sources; use non-sparking tools")
|
|
214
|
+
controls.append("Ground and bond all containers to prevent static discharge")
|
|
215
|
+
|
|
216
|
+
if "H250" in all_hazards or "H260" in all_hazards:
|
|
217
|
+
controls.append("Schlenk line or glovebox required for air/moisture-sensitive reagents")
|
|
218
|
+
|
|
219
|
+
if "GHS03" in all_pictograms:
|
|
220
|
+
controls.append("Keep oxidizers separated from fuels and organic materials")
|
|
221
|
+
controls.append("Fire suppression system accessible")
|
|
222
|
+
|
|
223
|
+
if any(h in all_hazards for h in ("H340", "H350", "H360")):
|
|
224
|
+
controls.append("Designated area for CMR (carcinogenic/mutagenic/reprotoxic) substances")
|
|
225
|
+
controls.append("HEPA-filtered ventilation for solid handling")
|
|
226
|
+
|
|
227
|
+
if "GHS09" in all_pictograms:
|
|
228
|
+
controls.append("Secondary containment to prevent environmental release")
|
|
229
|
+
controls.append("Do not dispose of via drain; collect all waste")
|
|
230
|
+
|
|
231
|
+
mean_t = (template.temperature_range[0] + template.temperature_range[1]) / 2.0
|
|
232
|
+
if mean_t < -40:
|
|
233
|
+
controls.append("Cryogenic cooling equipment; ensure adequate ventilation for cryogen vapours")
|
|
234
|
+
if mean_t > 150:
|
|
235
|
+
controls.append("High-temperature operation; thermal insulation and burn protection required")
|
|
236
|
+
|
|
237
|
+
return controls
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _determine_emergency_procedures(
|
|
241
|
+
all_hazards: set[str],
|
|
242
|
+
all_pictograms: set[str],
|
|
243
|
+
) -> list[str]:
|
|
244
|
+
"""Emergency response procedures based on hazard profile."""
|
|
245
|
+
procedures: list[str] = []
|
|
246
|
+
|
|
247
|
+
procedures.append(
|
|
248
|
+
"In case of spill: evacuate area, ventilate, absorb with "
|
|
249
|
+
"inert material (vermiculite), dispose as hazardous waste."
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if "GHS06" in all_pictograms or "H300" in all_hazards:
|
|
253
|
+
procedures.append(
|
|
254
|
+
"Ingestion: Do NOT induce vomiting. Call Poison Control / "
|
|
255
|
+
"emergency services immediately. Rinse mouth with water."
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if "H310" in all_hazards or "H311" in all_hazards:
|
|
259
|
+
procedures.append(
|
|
260
|
+
"Skin contact: Remove contaminated clothing immediately. "
|
|
261
|
+
"Wash skin with copious water for at least 15 minutes. "
|
|
262
|
+
"Seek medical attention."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if "H330" in all_hazards:
|
|
266
|
+
procedures.append(
|
|
267
|
+
"Inhalation: Move victim to fresh air. If not breathing, "
|
|
268
|
+
"administer artificial respiration. Call emergency services."
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if "H314" in all_hazards or "H318" in all_hazards:
|
|
272
|
+
procedures.append(
|
|
273
|
+
"Eye contact: Flush eyes with water for at least 15 minutes, "
|
|
274
|
+
"lifting upper and lower eyelids. Seek ophthalmological evaluation."
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
if "GHS02" in all_pictograms or "H250" in all_hazards:
|
|
278
|
+
procedures.append(
|
|
279
|
+
"Fire: Use dry chemical, CO2, or sand extinguisher. "
|
|
280
|
+
"Do NOT use water on pyrophoric / water-reactive materials. "
|
|
281
|
+
"Evacuate if fire cannot be controlled immediately."
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
if "H260" in all_hazards:
|
|
285
|
+
procedures.append(
|
|
286
|
+
"Water-reactive material: In case of spill, cover with dry "
|
|
287
|
+
"sand or vermiculite. Do NOT use water."
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
return procedures
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _determine_incompatibilities(template: ReactionTemplate) -> list[str]:
|
|
294
|
+
"""List known incompatible material pairs in the step."""
|
|
295
|
+
incompatibilities: list[str] = []
|
|
296
|
+
|
|
297
|
+
reagent_keys = {normalize_reagent_name(r) for r in template.reagents}
|
|
298
|
+
|
|
299
|
+
# Oxidizer + organic / reducer
|
|
300
|
+
oxidizers = {"kmno4", "cro3", "h2o2", "naocl", "mcpba", "hno3", "naio4"}
|
|
301
|
+
reducers = {"nabh4", "lialh4", "dibal_h", "na_nh3", "red_al", "nah"}
|
|
302
|
+
if reagent_keys & oxidizers and reagent_keys & reducers:
|
|
303
|
+
incompatibilities.append("CRITICAL: Oxidizer and reducer present -- do NOT mix directly")
|
|
304
|
+
|
|
305
|
+
# Acid + cyanide
|
|
306
|
+
acids = {"hcl", "h2so4", "hno3", "tfa", "acoh", "bf3_oet2"}
|
|
307
|
+
cyanides = {"nacn"}
|
|
308
|
+
if reagent_keys & acids and reagent_keys & cyanides:
|
|
309
|
+
incompatibilities.append("CRITICAL: Acid + cyanide can liberate HCN gas (fatal)")
|
|
310
|
+
|
|
311
|
+
# Acid + azide
|
|
312
|
+
azides = {"nan3"}
|
|
313
|
+
if reagent_keys & acids and reagent_keys & azides:
|
|
314
|
+
incompatibilities.append("CRITICAL: Acid + azide can liberate HN3 (explosive/toxic)")
|
|
315
|
+
|
|
316
|
+
# Water-reactive + aqueous
|
|
317
|
+
water_reactive = {"lialh4", "nah", "n_buli", "memgbr", "etmgbr", "phmgbr",
|
|
318
|
+
"meli", "phli", "socl2", "accl", "ticl4", "pcl5"}
|
|
319
|
+
if reagent_keys & water_reactive:
|
|
320
|
+
incompatibilities.append(
|
|
321
|
+
"Water-reactive reagent(s) present: ensure strictly anhydrous conditions"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Peroxides + metals
|
|
325
|
+
peroxides = {"h2o2", "mcpba", "tbhp", "dtbp", "benzoyl_peroxide"}
|
|
326
|
+
metals = {"ticl4", "alcl3", "zncl2", "cui", "fecl3", "fecl2"}
|
|
327
|
+
if reagent_keys & peroxides and reagent_keys & metals:
|
|
328
|
+
incompatibilities.append("Peroxide + metal salt: risk of uncontrolled decomposition")
|
|
329
|
+
|
|
330
|
+
# Hypochlorite + acids -> chlorine gas
|
|
331
|
+
hypochlorites = {"naocl", "bleach", "calcium_hypochlorite"}
|
|
332
|
+
if reagent_keys & hypochlorites and reagent_keys & acids:
|
|
333
|
+
incompatibilities.append("CRITICAL: Hypochlorite + acid liberates Cl2 gas (toxic)")
|
|
334
|
+
|
|
335
|
+
# Permanganate + concentrated organics -> fire/explosion
|
|
336
|
+
permanganates = {"kmno4", "namno4"}
|
|
337
|
+
flammable_organics = {"acetone", "diethyl_ether", "thf", "ethanol", "methanol",
|
|
338
|
+
"toluene", "hexane", "pentane", "dcm", "dmf", "dmso"}
|
|
339
|
+
if reagent_keys & permanganates and reagent_keys & flammable_organics:
|
|
340
|
+
incompatibilities.append("Permanganate + flammable organic: fire/explosion risk")
|
|
341
|
+
|
|
342
|
+
# Alkali metals + water -> violent reaction
|
|
343
|
+
alkali_metals = {"na", "k", "li", "cs", "nah", "kh"}
|
|
344
|
+
aqueous = {"h2o", "water", "naoh_aq", "hcl_aq"}
|
|
345
|
+
if reagent_keys & alkali_metals and reagent_keys & aqueous:
|
|
346
|
+
incompatibilities.append("CRITICAL: Alkali metal/hydride + water -> violent H2 evolution")
|
|
347
|
+
|
|
348
|
+
# Chlorine/halogens + ammonia -> toxic gases
|
|
349
|
+
halogens = {"cl2", "br2", "i2", "ncs"}
|
|
350
|
+
ammonia = {"nh3", "nh4oh", "nh4cl"}
|
|
351
|
+
if reagent_keys & halogens and reagent_keys & ammonia:
|
|
352
|
+
incompatibilities.append("CRITICAL: Halogen + ammonia -> toxic NCl3/NBr3")
|
|
353
|
+
|
|
354
|
+
# Strong oxidizers + flammable solvents
|
|
355
|
+
strong_oxidizers = {"kmno4", "cro3", "k2cr2o7", "hno3_conc", "h2o2_conc",
|
|
356
|
+
"naio4", "oxone", "dmp"}
|
|
357
|
+
if reagent_keys & strong_oxidizers and reagent_keys & flammable_organics:
|
|
358
|
+
incompatibilities.append("Strong oxidizer + flammable solvent: fire risk -- use compatible solvent")
|
|
359
|
+
|
|
360
|
+
# Nitrates + organics -> explosion risk
|
|
361
|
+
nitrates = {"nano3", "kno3", "agno3", "nh4no3"}
|
|
362
|
+
if reagent_keys & nitrates and reagent_keys & flammable_organics:
|
|
363
|
+
incompatibilities.append("Nitrate salt + organic solvent: explosion risk at elevated temperature")
|
|
364
|
+
|
|
365
|
+
# Concentrated acids + concentrated bases -> exothermic
|
|
366
|
+
bases = {"naoh", "koh", "lioh", "naoh_conc", "koh_conc", "nah", "naoh_aq"}
|
|
367
|
+
conc_acids = {"h2so4", "hno3", "hcl_conc", "h3po4", "hf"}
|
|
368
|
+
if reagent_keys & conc_acids and reagent_keys & bases:
|
|
369
|
+
incompatibilities.append("Concentrated acid + base: highly exothermic neutralization -- add slowly with cooling")
|
|
370
|
+
|
|
371
|
+
# Grignard/organolithium + protic solvents
|
|
372
|
+
organometallics = {"memgbr", "etmgbr", "phmgbr", "meli", "phli",
|
|
373
|
+
"n_buli", "t_buli", "s_buli", "znet2", "znme2"}
|
|
374
|
+
protic_solvents = {"meoh", "etoh", "h2o", "acoh", "ipoh"}
|
|
375
|
+
if reagent_keys & organometallics and reagent_keys & protic_solvents:
|
|
376
|
+
incompatibilities.append("Organometallic + protic solvent: immediate quench -- use ethereal solvents only")
|
|
377
|
+
|
|
378
|
+
# Perchloric acid + organics -> explosive perchlorates
|
|
379
|
+
perchlorics = {"hclo4", "perchloric_acid"}
|
|
380
|
+
if reagent_keys & perchlorics and reagent_keys & flammable_organics:
|
|
381
|
+
incompatibilities.append("CRITICAL: Perchloric acid + organics -> explosive perchlorate esters")
|
|
382
|
+
|
|
383
|
+
return incompatibilities
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _classify_waste(all_hazards: set[str]) -> str:
|
|
387
|
+
"""Classify waste stream based on worst hazard codes."""
|
|
388
|
+
if any(h in all_hazards for h in ("H300", "H310", "H330", "H340", "H350", "H360")):
|
|
389
|
+
return "Hazardous waste -- Category 1 (acute/CMR): requires licensed disposal contractor"
|
|
390
|
+
if any(h in all_hazards for h in ("H301", "H311", "H314", "H331", "H400", "H410")):
|
|
391
|
+
return "Hazardous waste -- Category 2 (toxic/corrosive/ecotoxic): segregated collection"
|
|
392
|
+
if any(h in all_hazards for h in ("H225", "H224", "H220", "H228")):
|
|
393
|
+
return "Hazardous waste -- flammable: store in approved flammable-waste containers"
|
|
394
|
+
return "Non-hazardous chemical waste: collect in appropriate waste stream"
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _calculate_risk_level(all_hazards: set[str], all_pictograms: set[str]) -> str:
|
|
398
|
+
"""Assign overall risk level for the step."""
|
|
399
|
+
# High risk: acutely fatal, CMR, pyrophoric, explosive
|
|
400
|
+
high_codes = {"H200", "H201", "H240", "H250", "H300", "H310", "H330",
|
|
401
|
+
"H340", "H350", "H360"}
|
|
402
|
+
if all_hazards & high_codes:
|
|
403
|
+
return "high"
|
|
404
|
+
|
|
405
|
+
# Medium risk: toxic, corrosive, flammable, sensitizer
|
|
406
|
+
medium_codes = {"H301", "H311", "H314", "H331", "H225", "H224",
|
|
407
|
+
"H260", "H334", "H317", "H370", "H372"}
|
|
408
|
+
if all_hazards & medium_codes:
|
|
409
|
+
return "medium"
|
|
410
|
+
|
|
411
|
+
return "low"
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# =====================================================================
|
|
415
|
+
# Public API
|
|
416
|
+
# =====================================================================
|
|
417
|
+
|
|
418
|
+
def assess_safety(steps: list[Any]) -> list[SafetyAssessment]:
|
|
419
|
+
"""Produce a :class:`SafetyAssessment` for every step in *steps*.
|
|
420
|
+
|
|
421
|
+
Parameters
|
|
422
|
+
----------
|
|
423
|
+
steps : list
|
|
424
|
+
Each element must have a ``.template`` attribute
|
|
425
|
+
(:class:`ReactionTemplate`) and a ``.precursors`` attribute.
|
|
426
|
+
Duck typing is used.
|
|
427
|
+
|
|
428
|
+
Returns
|
|
429
|
+
-------
|
|
430
|
+
list[SafetyAssessment]
|
|
431
|
+
One assessment per step, in order.
|
|
432
|
+
"""
|
|
433
|
+
if not steps:
|
|
434
|
+
return []
|
|
435
|
+
for i, step in enumerate(steps):
|
|
436
|
+
if not hasattr(step, 'template'):
|
|
437
|
+
raise TypeError(
|
|
438
|
+
f"Step {i} must have a 'template' attribute, "
|
|
439
|
+
f"got {type(step).__name__}"
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
assessments: list[SafetyAssessment] = []
|
|
443
|
+
|
|
444
|
+
for idx, step in enumerate(steps):
|
|
445
|
+
template: ReactionTemplate = step.template
|
|
446
|
+
|
|
447
|
+
# Collect hazard info for all reagents + catalysts
|
|
448
|
+
all_reagent_names = list(template.reagents) + list(template.catalysts)
|
|
449
|
+
hazard_infos: list[HazardInfo] = [
|
|
450
|
+
_build_hazard_info(rname) for rname in all_reagent_names
|
|
451
|
+
]
|
|
452
|
+
|
|
453
|
+
# Aggregate hazard codes and pictograms across all reagents in this step
|
|
454
|
+
all_hazards: set[str] = set()
|
|
455
|
+
all_pictograms: set[str] = set()
|
|
456
|
+
for hi in hazard_infos:
|
|
457
|
+
all_hazards.update(hi.ghs_hazards)
|
|
458
|
+
all_pictograms.update(hi.ghs_pictograms)
|
|
459
|
+
|
|
460
|
+
assessments.append(SafetyAssessment(
|
|
461
|
+
step_number=idx + 1,
|
|
462
|
+
step_name=template.name,
|
|
463
|
+
hazards=hazard_infos,
|
|
464
|
+
ppe_required=_determine_ppe(all_hazards, all_pictograms),
|
|
465
|
+
engineering_controls=_determine_engineering_controls(
|
|
466
|
+
all_hazards, all_pictograms, template,
|
|
467
|
+
),
|
|
468
|
+
emergency_procedures=_determine_emergency_procedures(
|
|
469
|
+
all_hazards, all_pictograms,
|
|
470
|
+
),
|
|
471
|
+
incompatible_materials=_determine_incompatibilities(template),
|
|
472
|
+
waste_classification=_classify_waste(all_hazards),
|
|
473
|
+
risk_level=_calculate_risk_level(all_hazards, all_pictograms),
|
|
474
|
+
))
|
|
475
|
+
|
|
476
|
+
return assessments
|