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,285 @@
|
|
|
1
|
+
"""Purification strategy recommendation for synthesis products.
|
|
2
|
+
|
|
3
|
+
Selects one or more :class:`PurificationStep` instances based on the reaction
|
|
4
|
+
type, product characteristics, and production scale.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum, auto
|
|
11
|
+
from typing import List
|
|
12
|
+
|
|
13
|
+
from molbuilder.reactions.reaction_types import ReactionCategory, ReactionTemplate
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# =====================================================================
|
|
17
|
+
# Enums and data classes
|
|
18
|
+
# =====================================================================
|
|
19
|
+
|
|
20
|
+
class PurificationMethod(Enum):
|
|
21
|
+
DISTILLATION = auto()
|
|
22
|
+
RECRYSTALLIZATION = auto()
|
|
23
|
+
COLUMN_CHROMATOGRAPHY = auto()
|
|
24
|
+
FLASH_CHROMATOGRAPHY = auto()
|
|
25
|
+
EXTRACTION = auto()
|
|
26
|
+
FILTRATION = auto()
|
|
27
|
+
PRECIPITATION = auto()
|
|
28
|
+
SUBLIMATION = auto()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class PurificationStep:
|
|
33
|
+
"""A single purification operation with expected performance."""
|
|
34
|
+
|
|
35
|
+
method: PurificationMethod
|
|
36
|
+
description: str
|
|
37
|
+
estimated_recovery: float # percent (0-100)
|
|
38
|
+
estimated_purity: float # percent (0-100)
|
|
39
|
+
scale_appropriate: bool
|
|
40
|
+
notes: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# =====================================================================
|
|
44
|
+
# Internal helpers
|
|
45
|
+
# =====================================================================
|
|
46
|
+
|
|
47
|
+
# Categories whose products are typically liquids at room temperature
|
|
48
|
+
_LIQUID_PRODUCT_CATEGORIES = {
|
|
49
|
+
ReactionCategory.ELIMINATION,
|
|
50
|
+
ReactionCategory.RADICAL,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Categories whose products are typically solids
|
|
54
|
+
_SOLID_PRODUCT_CATEGORIES = {
|
|
55
|
+
ReactionCategory.COUPLING,
|
|
56
|
+
ReactionCategory.PROTECTION,
|
|
57
|
+
ReactionCategory.CARBONYL,
|
|
58
|
+
ReactionCategory.PERICYCLIC,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Categories that often produce complex mixtures requiring chromatography
|
|
62
|
+
_COMPLEX_MIXTURE_CATEGORIES = {
|
|
63
|
+
ReactionCategory.REARRANGEMENT,
|
|
64
|
+
ReactionCategory.RADICAL,
|
|
65
|
+
ReactionCategory.MISC,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _chromatography_appropriate(scale_kg: float) -> bool:
|
|
70
|
+
"""Chromatography is generally impractical above ~5 kg scale."""
|
|
71
|
+
return scale_kg <= 5.0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _extraction_step(scale_kg: float) -> PurificationStep:
|
|
75
|
+
return PurificationStep(
|
|
76
|
+
method=PurificationMethod.EXTRACTION,
|
|
77
|
+
description=(
|
|
78
|
+
"Liquid-liquid extraction with aqueous wash (saturated NaHCO3, "
|
|
79
|
+
"then brine) to remove polar impurities and inorganic salts."
|
|
80
|
+
),
|
|
81
|
+
estimated_recovery=92.0,
|
|
82
|
+
estimated_purity=70.0,
|
|
83
|
+
scale_appropriate=True,
|
|
84
|
+
notes="Use separating funnel (lab) or mixer-settler (plant).",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _distillation_step(scale_kg: float) -> PurificationStep:
|
|
89
|
+
return PurificationStep(
|
|
90
|
+
method=PurificationMethod.DISTILLATION,
|
|
91
|
+
description=(
|
|
92
|
+
"Simple or fractional distillation under reduced pressure "
|
|
93
|
+
"if product bp is below 200 degC. Use short-path distillation "
|
|
94
|
+
"for heat-sensitive materials."
|
|
95
|
+
),
|
|
96
|
+
estimated_recovery=85.0,
|
|
97
|
+
estimated_purity=95.0,
|
|
98
|
+
scale_appropriate=True,
|
|
99
|
+
notes=(
|
|
100
|
+
"Highly scalable. Ensure delta-bp between product and "
|
|
101
|
+
"impurities is >15 degC for simple distillation."
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _recrystallization_step(scale_kg: float) -> PurificationStep:
|
|
107
|
+
return PurificationStep(
|
|
108
|
+
method=PurificationMethod.RECRYSTALLIZATION,
|
|
109
|
+
description=(
|
|
110
|
+
"Dissolve crude in minimum hot solvent (e.g. ethanol, ethyl "
|
|
111
|
+
"acetate, or toluene), filter hot, cool slowly to crystallise. "
|
|
112
|
+
"Collect crystals by vacuum filtration."
|
|
113
|
+
),
|
|
114
|
+
estimated_recovery=75.0,
|
|
115
|
+
estimated_purity=97.0,
|
|
116
|
+
scale_appropriate=True,
|
|
117
|
+
notes="Solvent screening recommended. May need 2 crops to maximise yield.",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _flash_chromatography_step(scale_kg: float) -> PurificationStep:
|
|
122
|
+
return PurificationStep(
|
|
123
|
+
method=PurificationMethod.FLASH_CHROMATOGRAPHY,
|
|
124
|
+
description=(
|
|
125
|
+
"Flash column chromatography on silica gel (40-63 um) with "
|
|
126
|
+
"gradient elution (e.g. hexanes/ethyl acetate)."
|
|
127
|
+
),
|
|
128
|
+
estimated_recovery=80.0,
|
|
129
|
+
estimated_purity=95.0,
|
|
130
|
+
scale_appropriate=_chromatography_appropriate(scale_kg),
|
|
131
|
+
notes=(
|
|
132
|
+
"Practical up to ~5 kg. Above that, consider preparative HPLC "
|
|
133
|
+
"or alternative purification strategies."
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _column_chromatography_step(scale_kg: float) -> PurificationStep:
|
|
139
|
+
return PurificationStep(
|
|
140
|
+
method=PurificationMethod.COLUMN_CHROMATOGRAPHY,
|
|
141
|
+
description=(
|
|
142
|
+
"Gravity column chromatography on silica gel. Suitable when "
|
|
143
|
+
"flash equipment is unavailable; slower but gentler."
|
|
144
|
+
),
|
|
145
|
+
estimated_recovery=75.0,
|
|
146
|
+
estimated_purity=93.0,
|
|
147
|
+
scale_appropriate=_chromatography_appropriate(scale_kg),
|
|
148
|
+
notes="Load ratio: ~30:1 silica-to-crude by weight.",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _filtration_step(scale_kg: float) -> PurificationStep:
|
|
153
|
+
return PurificationStep(
|
|
154
|
+
method=PurificationMethod.FILTRATION,
|
|
155
|
+
description=(
|
|
156
|
+
"Vacuum filtration through Celite or sintered-glass funnel "
|
|
157
|
+
"to remove catalyst residues and insoluble by-products."
|
|
158
|
+
),
|
|
159
|
+
estimated_recovery=95.0,
|
|
160
|
+
estimated_purity=60.0,
|
|
161
|
+
scale_appropriate=True,
|
|
162
|
+
notes="Often the first purification step for heterogeneous reactions.",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _precipitation_step(scale_kg: float) -> PurificationStep:
|
|
167
|
+
return PurificationStep(
|
|
168
|
+
method=PurificationMethod.PRECIPITATION,
|
|
169
|
+
description=(
|
|
170
|
+
"Add anti-solvent (water, hexanes, or diethyl ether) to "
|
|
171
|
+
"precipitate product from solution. Collect by filtration."
|
|
172
|
+
),
|
|
173
|
+
estimated_recovery=80.0,
|
|
174
|
+
estimated_purity=88.0,
|
|
175
|
+
scale_appropriate=True,
|
|
176
|
+
notes="Works best when product is much less soluble than impurities in the anti-solvent.",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _sublimation_step(scale_kg: float) -> PurificationStep:
|
|
181
|
+
return PurificationStep(
|
|
182
|
+
method=PurificationMethod.SUBLIMATION,
|
|
183
|
+
description=(
|
|
184
|
+
"Vacuum sublimation at reduced pressure. Best for low-MW "
|
|
185
|
+
"solids with high vapour pressure (e.g. naphthalene, ferrocene)."
|
|
186
|
+
),
|
|
187
|
+
estimated_recovery=70.0,
|
|
188
|
+
estimated_purity=99.0,
|
|
189
|
+
scale_appropriate=scale_kg <= 1.0,
|
|
190
|
+
notes="Limited throughput; primarily a lab-scale technique.",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# =====================================================================
|
|
195
|
+
# Public API
|
|
196
|
+
# =====================================================================
|
|
197
|
+
|
|
198
|
+
def recommend_purification(
|
|
199
|
+
template: ReactionTemplate,
|
|
200
|
+
scale_kg: float,
|
|
201
|
+
) -> list[PurificationStep]:
|
|
202
|
+
"""Return an ordered list of purification steps for *template* at *scale_kg*.
|
|
203
|
+
|
|
204
|
+
Strategy
|
|
205
|
+
--------
|
|
206
|
+
* Liquid products -> extraction then distillation
|
|
207
|
+
* Solid products -> extraction then recrystallization
|
|
208
|
+
* Complex mixtures -> extraction + chromatography (small scale) or
|
|
209
|
+
extraction + precipitation + recrystallization (large scale)
|
|
210
|
+
* Catalytic reactions always start with filtration
|
|
211
|
+
* Large scale avoids chromatography
|
|
212
|
+
"""
|
|
213
|
+
if not hasattr(template, 'category'):
|
|
214
|
+
raise TypeError(
|
|
215
|
+
f"template must have a 'category' attribute, "
|
|
216
|
+
f"got {type(template).__name__}"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
steps: list[PurificationStep] = []
|
|
220
|
+
cat = template.category
|
|
221
|
+
has_catalyst = len(template.catalysts) > 0
|
|
222
|
+
|
|
223
|
+
# --- Step 0: Filtration for catalytic reactions ---
|
|
224
|
+
if has_catalyst:
|
|
225
|
+
steps.append(_filtration_step(scale_kg))
|
|
226
|
+
|
|
227
|
+
# --- Liquid products ---
|
|
228
|
+
if cat in _LIQUID_PRODUCT_CATEGORIES:
|
|
229
|
+
steps.append(_extraction_step(scale_kg))
|
|
230
|
+
steps.append(_distillation_step(scale_kg))
|
|
231
|
+
return steps
|
|
232
|
+
|
|
233
|
+
# --- Complex mixtures ---
|
|
234
|
+
if cat in _COMPLEX_MIXTURE_CATEGORIES:
|
|
235
|
+
steps.append(_extraction_step(scale_kg))
|
|
236
|
+
if _chromatography_appropriate(scale_kg):
|
|
237
|
+
steps.append(_flash_chromatography_step(scale_kg))
|
|
238
|
+
else:
|
|
239
|
+
steps.append(_precipitation_step(scale_kg))
|
|
240
|
+
steps.append(_recrystallization_step(scale_kg))
|
|
241
|
+
return steps
|
|
242
|
+
|
|
243
|
+
# --- Solid products ---
|
|
244
|
+
if cat in _SOLID_PRODUCT_CATEGORIES:
|
|
245
|
+
steps.append(_extraction_step(scale_kg))
|
|
246
|
+
steps.append(_recrystallization_step(scale_kg))
|
|
247
|
+
return steps
|
|
248
|
+
|
|
249
|
+
# --- Oxidation / Reduction: often aqueous workup + extraction ---
|
|
250
|
+
if cat in {ReactionCategory.OXIDATION, ReactionCategory.REDUCTION}:
|
|
251
|
+
steps.append(_extraction_step(scale_kg))
|
|
252
|
+
if _chromatography_appropriate(scale_kg) and scale_kg < 0.5:
|
|
253
|
+
steps.append(_flash_chromatography_step(scale_kg))
|
|
254
|
+
else:
|
|
255
|
+
steps.append(_distillation_step(scale_kg))
|
|
256
|
+
return steps
|
|
257
|
+
|
|
258
|
+
# --- Substitution / Addition: general workflow ---
|
|
259
|
+
if cat in {ReactionCategory.SUBSTITUTION, ReactionCategory.ADDITION}:
|
|
260
|
+
steps.append(_extraction_step(scale_kg))
|
|
261
|
+
if _chromatography_appropriate(scale_kg) and scale_kg < 1.0:
|
|
262
|
+
steps.append(_flash_chromatography_step(scale_kg))
|
|
263
|
+
else:
|
|
264
|
+
steps.append(_distillation_step(scale_kg))
|
|
265
|
+
return steps
|
|
266
|
+
|
|
267
|
+
# --- Deprotection ---
|
|
268
|
+
if cat == ReactionCategory.DEPROTECTION:
|
|
269
|
+
steps.append(_extraction_step(scale_kg))
|
|
270
|
+
steps.append(_precipitation_step(scale_kg))
|
|
271
|
+
return steps
|
|
272
|
+
|
|
273
|
+
# --- Polymerization ---
|
|
274
|
+
if cat == ReactionCategory.POLYMERIZATION:
|
|
275
|
+
steps.append(_precipitation_step(scale_kg))
|
|
276
|
+
steps.append(_filtration_step(scale_kg))
|
|
277
|
+
return steps
|
|
278
|
+
|
|
279
|
+
# --- Default fallback ---
|
|
280
|
+
steps.append(_extraction_step(scale_kg))
|
|
281
|
+
if _chromatography_appropriate(scale_kg):
|
|
282
|
+
steps.append(_flash_chromatography_step(scale_kg))
|
|
283
|
+
else:
|
|
284
|
+
steps.append(_recrystallization_step(scale_kg))
|
|
285
|
+
return steps
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Reactor selection and specification for process engineering.
|
|
2
|
+
|
|
3
|
+
Maps reaction characteristics (exothermicity, phase, scale) to an appropriate
|
|
4
|
+
reactor type and provides a fully populated :class:`ReactorSpec` dataclass.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum, auto
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from molbuilder.reactions.reaction_types import ReactionCategory, ReactionTemplate
|
|
14
|
+
from molbuilder.reactions.reagent_data import normalize_reagent_name
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# =====================================================================
|
|
18
|
+
# Enums
|
|
19
|
+
# =====================================================================
|
|
20
|
+
|
|
21
|
+
class ReactorType(Enum):
|
|
22
|
+
BATCH = auto()
|
|
23
|
+
SEMI_BATCH = auto()
|
|
24
|
+
CSTR = auto() # Continuous stirred-tank reactor
|
|
25
|
+
PFR = auto() # Plug flow reactor
|
|
26
|
+
MICROREACTOR = auto()
|
|
27
|
+
FIXED_BED = auto()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# =====================================================================
|
|
31
|
+
# Reactor specification
|
|
32
|
+
# =====================================================================
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ReactorSpec:
|
|
36
|
+
"""Complete specification of a process reactor."""
|
|
37
|
+
|
|
38
|
+
reactor_type: ReactorType
|
|
39
|
+
volume_L: float
|
|
40
|
+
temperature_C: float
|
|
41
|
+
pressure_atm: float
|
|
42
|
+
residence_time_min: float
|
|
43
|
+
mixing_type: str # "mechanical", "static", "none"
|
|
44
|
+
heat_transfer: str # "jacketed", "coil", "adiabatic"
|
|
45
|
+
material: str # "glass", "stainless steel", "hastelloy"
|
|
46
|
+
estimated_cost_usd: float
|
|
47
|
+
notes: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# =====================================================================
|
|
51
|
+
# Internal helpers
|
|
52
|
+
# =====================================================================
|
|
53
|
+
|
|
54
|
+
# Categories that are typically highly exothermic or fast
|
|
55
|
+
_EXOTHERMIC_CATEGORIES = {
|
|
56
|
+
ReactionCategory.ADDITION,
|
|
57
|
+
ReactionCategory.RADICAL,
|
|
58
|
+
ReactionCategory.POLYMERIZATION,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Categories that are often multiphase (solid catalyst, heterogeneous)
|
|
62
|
+
_MULTIPHASE_CATEGORIES = {
|
|
63
|
+
ReactionCategory.COUPLING,
|
|
64
|
+
ReactionCategory.REDUCTION,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Categories commonly run under inert or pressurised conditions
|
|
68
|
+
_HIGH_PRESSURE_CATEGORIES = {
|
|
69
|
+
ReactionCategory.REDUCTION,
|
|
70
|
+
ReactionCategory.POLYMERIZATION,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _mean_temp(template: ReactionTemplate) -> float:
|
|
75
|
+
"""Return the midpoint of the template temperature range."""
|
|
76
|
+
lo, hi = template.temperature_range
|
|
77
|
+
return (lo + hi) / 2.0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _is_cryogenic(template: ReactionTemplate) -> bool:
|
|
81
|
+
return _mean_temp(template) < -20.0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _is_high_temp(template: ReactionTemplate) -> bool:
|
|
85
|
+
return _mean_temp(template) > 150.0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _select_material(template: ReactionTemplate) -> str:
|
|
89
|
+
"""Choose vessel material based on temperature and corrosive reagents."""
|
|
90
|
+
corrosive_keywords = {"hcl", "h2so4", "hno3", "hf", "tfa", "socl2", "ticl4"}
|
|
91
|
+
reagent_keys = {normalize_reagent_name(r) for r in template.reagents}
|
|
92
|
+
if reagent_keys & corrosive_keywords:
|
|
93
|
+
return "hastelloy"
|
|
94
|
+
if _is_high_temp(template):
|
|
95
|
+
return "stainless steel"
|
|
96
|
+
return "glass"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _estimate_reactor_cost(reactor_type: ReactorType, volume_L: float) -> float:
|
|
100
|
+
"""Rough capital cost estimate in USD.
|
|
101
|
+
|
|
102
|
+
Based on typical 2024 equipment pricing for chemical process vessels.
|
|
103
|
+
"""
|
|
104
|
+
base_costs = {
|
|
105
|
+
ReactorType.BATCH: 8_000,
|
|
106
|
+
ReactorType.SEMI_BATCH: 12_000,
|
|
107
|
+
ReactorType.CSTR: 25_000,
|
|
108
|
+
ReactorType.PFR: 30_000,
|
|
109
|
+
ReactorType.MICROREACTOR: 50_000,
|
|
110
|
+
ReactorType.FIXED_BED: 35_000,
|
|
111
|
+
}
|
|
112
|
+
base = base_costs.get(reactor_type, 10_000)
|
|
113
|
+
# Scale by volume using the six-tenths rule (cost ~ volume^0.6)
|
|
114
|
+
reference_volume = 100.0 # litres
|
|
115
|
+
if volume_L <= 0:
|
|
116
|
+
volume_L = 1.0
|
|
117
|
+
scale_factor = (volume_L / reference_volume) ** 0.6
|
|
118
|
+
return round(base * max(scale_factor, 0.3), -2)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _volume_for_scale(scale_kg: float, concentration_factor: float = 5.0) -> float:
|
|
122
|
+
"""Estimate vessel volume in litres.
|
|
123
|
+
|
|
124
|
+
Assumes ~5 L of solvent+reagent per kg of product (tuneable via
|
|
125
|
+
*concentration_factor*) and a 75 % fill level.
|
|
126
|
+
"""
|
|
127
|
+
raw = scale_kg * concentration_factor
|
|
128
|
+
return round(raw / 0.75, 1)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# =====================================================================
|
|
132
|
+
# Public API
|
|
133
|
+
# =====================================================================
|
|
134
|
+
|
|
135
|
+
def select_reactor(
|
|
136
|
+
template: ReactionTemplate,
|
|
137
|
+
scale_kg: float,
|
|
138
|
+
) -> ReactorSpec:
|
|
139
|
+
"""Select an appropriate reactor for *template* at *scale_kg*.
|
|
140
|
+
|
|
141
|
+
Decision tree
|
|
142
|
+
-------------
|
|
143
|
+
1. Fast / exothermic at small scale -> MICROREACTOR
|
|
144
|
+
2. Fast / exothermic at large scale -> CSTR with jacketed cooling
|
|
145
|
+
3. Slow multiphase or catalytic -> BATCH (small) or FIXED_BED (large)
|
|
146
|
+
4. High-volume commodity (>500 kg) -> PFR
|
|
147
|
+
5. Moderate scale, needs controlled -> SEMI_BATCH
|
|
148
|
+
addition
|
|
149
|
+
6. Default -> BATCH
|
|
150
|
+
"""
|
|
151
|
+
if not hasattr(template, 'temperature_range'):
|
|
152
|
+
raise TypeError(
|
|
153
|
+
f"template must have a 'temperature_range' attribute, "
|
|
154
|
+
f"got {type(template).__name__}"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
mean_t = _mean_temp(template)
|
|
158
|
+
volume = _volume_for_scale(scale_kg)
|
|
159
|
+
material = _select_material(template)
|
|
160
|
+
|
|
161
|
+
is_exothermic = template.category in _EXOTHERMIC_CATEGORIES
|
|
162
|
+
is_multiphase = template.category in _MULTIPHASE_CATEGORIES
|
|
163
|
+
has_catalyst = len(template.catalysts) > 0
|
|
164
|
+
|
|
165
|
+
# --- 1. Fast exothermic, small scale -> microreactor ---
|
|
166
|
+
if is_exothermic and scale_kg < 1.0:
|
|
167
|
+
rt = ReactorType.MICROREACTOR
|
|
168
|
+
vol = max(0.05, scale_kg * 0.5)
|
|
169
|
+
return ReactorSpec(
|
|
170
|
+
reactor_type=rt,
|
|
171
|
+
volume_L=vol,
|
|
172
|
+
temperature_C=mean_t,
|
|
173
|
+
pressure_atm=1.0 if mean_t < 100 else 2.0,
|
|
174
|
+
residence_time_min=2.0,
|
|
175
|
+
mixing_type="static",
|
|
176
|
+
heat_transfer="coil",
|
|
177
|
+
material="stainless steel",
|
|
178
|
+
estimated_cost_usd=_estimate_reactor_cost(rt, vol),
|
|
179
|
+
notes=(
|
|
180
|
+
"Microreactor recommended for fast exothermic reaction at "
|
|
181
|
+
"sub-kilogram scale. Excellent heat removal and mixing."
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# --- 2. Fast exothermic, large scale -> CSTR ---
|
|
186
|
+
if is_exothermic and scale_kg >= 1.0:
|
|
187
|
+
rt = ReactorType.CSTR
|
|
188
|
+
return ReactorSpec(
|
|
189
|
+
reactor_type=rt,
|
|
190
|
+
volume_L=volume,
|
|
191
|
+
temperature_C=mean_t,
|
|
192
|
+
pressure_atm=1.0 if mean_t < 100 else 3.0,
|
|
193
|
+
residence_time_min=30.0,
|
|
194
|
+
mixing_type="mechanical",
|
|
195
|
+
heat_transfer="jacketed",
|
|
196
|
+
material=material,
|
|
197
|
+
estimated_cost_usd=_estimate_reactor_cost(rt, volume),
|
|
198
|
+
notes=(
|
|
199
|
+
"CSTR selected for exothermic reaction at production scale. "
|
|
200
|
+
"Jacket cooling essential; consider cascade of 2-3 CSTRs for "
|
|
201
|
+
"improved conversion."
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# --- 3. Multiphase / catalytic ---
|
|
206
|
+
if is_multiphase or has_catalyst:
|
|
207
|
+
if scale_kg > 100.0 and has_catalyst:
|
|
208
|
+
rt = ReactorType.FIXED_BED
|
|
209
|
+
return ReactorSpec(
|
|
210
|
+
reactor_type=rt,
|
|
211
|
+
volume_L=volume,
|
|
212
|
+
temperature_C=mean_t,
|
|
213
|
+
pressure_atm=3.0,
|
|
214
|
+
residence_time_min=15.0,
|
|
215
|
+
mixing_type="none",
|
|
216
|
+
heat_transfer="coil",
|
|
217
|
+
material=material,
|
|
218
|
+
estimated_cost_usd=_estimate_reactor_cost(rt, volume),
|
|
219
|
+
notes=(
|
|
220
|
+
"Fixed-bed reactor for heterogeneous catalytic process "
|
|
221
|
+
"at >100 kg scale. Catalyst lifetime and regeneration "
|
|
222
|
+
"strategy must be defined."
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
rt = ReactorType.BATCH
|
|
226
|
+
return ReactorSpec(
|
|
227
|
+
reactor_type=rt,
|
|
228
|
+
volume_L=volume,
|
|
229
|
+
temperature_C=mean_t,
|
|
230
|
+
pressure_atm=1.0,
|
|
231
|
+
residence_time_min=120.0,
|
|
232
|
+
mixing_type="mechanical",
|
|
233
|
+
heat_transfer="jacketed",
|
|
234
|
+
material=material,
|
|
235
|
+
estimated_cost_usd=_estimate_reactor_cost(rt, volume),
|
|
236
|
+
notes=(
|
|
237
|
+
"Batch reactor for multiphase or catalytic reaction. "
|
|
238
|
+
"Ensure adequate agitation for mass transfer."
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# --- 4. High-volume commodity -> PFR ---
|
|
243
|
+
if scale_kg > 500.0:
|
|
244
|
+
rt = ReactorType.PFR
|
|
245
|
+
return ReactorSpec(
|
|
246
|
+
reactor_type=rt,
|
|
247
|
+
volume_L=volume,
|
|
248
|
+
temperature_C=mean_t,
|
|
249
|
+
pressure_atm=5.0,
|
|
250
|
+
residence_time_min=20.0,
|
|
251
|
+
mixing_type="static",
|
|
252
|
+
heat_transfer="coil",
|
|
253
|
+
material="stainless steel",
|
|
254
|
+
estimated_cost_usd=_estimate_reactor_cost(rt, volume),
|
|
255
|
+
notes=(
|
|
256
|
+
"Plug-flow reactor for high-volume continuous production. "
|
|
257
|
+
"Back-mixing minimised; good for high conversion targets."
|
|
258
|
+
),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# --- 5. Controlled addition needed -> semi-batch ---
|
|
262
|
+
needs_slow_addition = (
|
|
263
|
+
template.category in {ReactionCategory.CARBONYL, ReactionCategory.SUBSTITUTION}
|
|
264
|
+
and scale_kg > 10.0
|
|
265
|
+
)
|
|
266
|
+
if needs_slow_addition:
|
|
267
|
+
rt = ReactorType.SEMI_BATCH
|
|
268
|
+
return ReactorSpec(
|
|
269
|
+
reactor_type=rt,
|
|
270
|
+
volume_L=volume,
|
|
271
|
+
temperature_C=mean_t,
|
|
272
|
+
pressure_atm=1.0,
|
|
273
|
+
residence_time_min=90.0,
|
|
274
|
+
mixing_type="mechanical",
|
|
275
|
+
heat_transfer="jacketed",
|
|
276
|
+
material=material,
|
|
277
|
+
estimated_cost_usd=_estimate_reactor_cost(rt, volume),
|
|
278
|
+
notes=(
|
|
279
|
+
"Semi-batch reactor allows controlled reagent addition to "
|
|
280
|
+
"manage selectivity and heat release."
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# --- 6. Default -> batch ---
|
|
285
|
+
rt = ReactorType.BATCH
|
|
286
|
+
return ReactorSpec(
|
|
287
|
+
reactor_type=rt,
|
|
288
|
+
volume_L=volume,
|
|
289
|
+
temperature_C=mean_t,
|
|
290
|
+
pressure_atm=1.0,
|
|
291
|
+
residence_time_min=60.0,
|
|
292
|
+
mixing_type="mechanical",
|
|
293
|
+
heat_transfer="jacketed" if volume > 20 else "adiabatic",
|
|
294
|
+
material=material,
|
|
295
|
+
estimated_cost_usd=_estimate_reactor_cost(rt, volume),
|
|
296
|
+
notes="Standard batch reactor; suitable for most laboratory and pilot-scale work.",
|
|
297
|
+
)
|