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,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
+ )