cdxml-toolkit 0.5.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 (91) hide show
  1. cdxml_toolkit/__init__.py +18 -0
  2. cdxml_toolkit/_jre/__init__.py +2 -0
  3. cdxml_toolkit/_jre/temurin-21-jre-win-x64.zip +0 -0
  4. cdxml_toolkit/analysis/__init__.py +35 -0
  5. cdxml_toolkit/analysis/deterministic/__init__.py +12 -0
  6. cdxml_toolkit/analysis/deterministic/discover_experiment_files.py +413 -0
  7. cdxml_toolkit/analysis/deterministic/lab_book_formatter.py +701 -0
  8. cdxml_toolkit/analysis/deterministic/lcms_file_categorizer.py +928 -0
  9. cdxml_toolkit/analysis/deterministic/lcms_identifier.py +598 -0
  10. cdxml_toolkit/analysis/deterministic/mass_resolver.py +654 -0
  11. cdxml_toolkit/analysis/deterministic/multi_lcms_analyzer.py +1412 -0
  12. cdxml_toolkit/analysis/deterministic/procedure_writer.py +446 -0
  13. cdxml_toolkit/analysis/extract_nmr.py +47 -0
  14. cdxml_toolkit/analysis/format_procedure_entry.py +479 -0
  15. cdxml_toolkit/analysis/lcms_analyzer.py +1299 -0
  16. cdxml_toolkit/analysis/parse_analysis_file.py +134 -0
  17. cdxml_toolkit/cdxml_builder.py +920 -0
  18. cdxml_toolkit/cdxml_utils.py +342 -0
  19. cdxml_toolkit/chemdraw/__init__.py +5 -0
  20. cdxml_toolkit/chemdraw/_chemscript_server.py +562 -0
  21. cdxml_toolkit/chemdraw/cdx_converter.py +527 -0
  22. cdxml_toolkit/chemdraw/cdxml_to_image.py +262 -0
  23. cdxml_toolkit/chemdraw/cdxml_to_image_rdkit.py +296 -0
  24. cdxml_toolkit/chemdraw/chemscript_bridge.py +901 -0
  25. cdxml_toolkit/constants.py +304 -0
  26. cdxml_toolkit/coord_normalizer.py +438 -0
  27. cdxml_toolkit/deterministic_pipeline/__init__.py +6 -0
  28. cdxml_toolkit/deterministic_pipeline/legacy/__init__.py +5 -0
  29. cdxml_toolkit/deterministic_pipeline/legacy/eln_cdx_cleanup.py +509 -0
  30. cdxml_toolkit/deterministic_pipeline/legacy/eln_enrichment.py +1394 -0
  31. cdxml_toolkit/deterministic_pipeline/legacy/scheme_aligner.py +428 -0
  32. cdxml_toolkit/deterministic_pipeline/legacy/scheme_polisher.py +1337 -0
  33. cdxml_toolkit/deterministic_pipeline/legacy/scheme_polisher_v2.py +1340 -0
  34. cdxml_toolkit/deterministic_pipeline/scheme_reader_audit.py +931 -0
  35. cdxml_toolkit/deterministic_pipeline/scheme_reader_verify.py +1160 -0
  36. cdxml_toolkit/image/__init__.py +15 -0
  37. cdxml_toolkit/image/reaction_from_image.py +2103 -0
  38. cdxml_toolkit/image/structure_from_image.py +1711 -0
  39. cdxml_toolkit/layout/__init__.py +5 -0
  40. cdxml_toolkit/layout/alignment.py +1642 -0
  41. cdxml_toolkit/layout/reaction_cleanup.py +1002 -0
  42. cdxml_toolkit/layout/scheme_merger.py +2260 -0
  43. cdxml_toolkit/mcp_server/__init__.py +0 -0
  44. cdxml_toolkit/mcp_server/__main__.py +5 -0
  45. cdxml_toolkit/mcp_server/server.py +1567 -0
  46. cdxml_toolkit/naming/__init__.py +6 -0
  47. cdxml_toolkit/naming/aligned_namer.py +2342 -0
  48. cdxml_toolkit/naming/mol_builder.py +3722 -0
  49. cdxml_toolkit/naming/name_decomposer.py +2843 -0
  50. cdxml_toolkit/naming/reactions_datamol.json +2414 -0
  51. cdxml_toolkit/office/__init__.py +5 -0
  52. cdxml_toolkit/office/doc_from_template.py +722 -0
  53. cdxml_toolkit/office/ole_embedder.py +808 -0
  54. cdxml_toolkit/office/ole_extractor.py +272 -0
  55. cdxml_toolkit/perception/__init__.py +10 -0
  56. cdxml_toolkit/perception/compound_search.py +229 -0
  57. cdxml_toolkit/perception/eln_csv_parser.py +240 -0
  58. cdxml_toolkit/perception/rdf_parser.py +664 -0
  59. cdxml_toolkit/perception/reactant_heuristic.py +1045 -0
  60. cdxml_toolkit/perception/reaction_parser.py +2150 -0
  61. cdxml_toolkit/perception/scheme_reader.py +2948 -0
  62. cdxml_toolkit/perception/scheme_refine.py +1404 -0
  63. cdxml_toolkit/perception/scheme_segmenter.py +619 -0
  64. cdxml_toolkit/perception/spatial_assignment.py +1013 -0
  65. cdxml_toolkit/rdkit_utils.py +605 -0
  66. cdxml_toolkit/render/__init__.py +17 -0
  67. cdxml_toolkit/render/auto_layout.py +229 -0
  68. cdxml_toolkit/render/compact_parser.py +632 -0
  69. cdxml_toolkit/render/parser.py +706 -0
  70. cdxml_toolkit/render/render_scheme.py +267 -0
  71. cdxml_toolkit/render/renderer.py +2387 -0
  72. cdxml_toolkit/render/schema.py +90 -0
  73. cdxml_toolkit/render/scheme_maker.py +1043 -0
  74. cdxml_toolkit/render/scheme_yaml_writer.py +1487 -0
  75. cdxml_toolkit/resolve/__init__.py +13 -0
  76. cdxml_toolkit/resolve/cas_resolver.py +430 -0
  77. cdxml_toolkit/resolve/chemscanner_abbreviations.json +28813 -0
  78. cdxml_toolkit/resolve/condensed_formula.py +493 -0
  79. cdxml_toolkit/resolve/jre_manager.py +195 -0
  80. cdxml_toolkit/resolve/reagent_abbreviations.json +1046 -0
  81. cdxml_toolkit/resolve/reagent_db.py +285 -0
  82. cdxml_toolkit/resolve/superatom_data.json +2856 -0
  83. cdxml_toolkit/resolve/superatom_table.py +146 -0
  84. cdxml_toolkit/text_formatting.py +298 -0
  85. cdxml_toolkit-0.5.0.dist-info/METADATA +318 -0
  86. cdxml_toolkit-0.5.0.dist-info/RECORD +91 -0
  87. cdxml_toolkit-0.5.0.dist-info/WHEEL +5 -0
  88. cdxml_toolkit-0.5.0.dist-info/entry_points.txt +17 -0
  89. cdxml_toolkit-0.5.0.dist-info/licenses/LICENSE +21 -0
  90. cdxml_toolkit-0.5.0.dist-info/licenses/NOTICE.md +37 -0
  91. cdxml_toolkit-0.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,438 @@
1
+ # NOTE: This module is not imported by any other tool as of v0.3.
2
+ # It is a standalone CLI utility.
3
+ #!/usr/bin/env python3
4
+ """
5
+ coord_normalizer.py — Normalize atom coordinates to ACS Document 1996 standard.
6
+
7
+ Takes atom coordinates from any source (RDKit, PubChem SDF, SciFinder RDF V3000 MOL
8
+ blocks) and normalizes them so they are ready for cdxml_builder.py:
9
+
10
+ - Scale so that the average bond length == 14.40 pt (ACS 1996 target)
11
+ - Flip y-axis (MOL/SDF format is y-up; CDXML is y-down)
12
+ - Center molecule at a caller-supplied (cx, cy) position
13
+ - Strip explicit hydrogens: remove H atoms and update NumHydrogens on the
14
+ heavy atom they were bonded to
15
+
16
+ The module can also be used as a CLI tool to normalise a JSON atom/bond file.
17
+
18
+ Usage (CLI):
19
+ python coord_normalizer.py molecule.json [options]
20
+ python coord_normalizer.py molecule.json --center 200 300 --output normalised.json
21
+
22
+ Input JSON format (same as cdxml_builder.py expects):
23
+ {
24
+ "atoms": [
25
+ {"index": 1, "symbol": "C", "x": 0.0, "y": 0.0},
26
+ ...
27
+ ],
28
+ "bonds": [
29
+ {"index": 1, "order": 1, "atom1": 1, "atom2": 2},
30
+ ...
31
+ ]
32
+ }
33
+
34
+ Output: same JSON structure with normalised x/y and added "num_hydrogens" fields.
35
+ """
36
+
37
+ import argparse
38
+ import json
39
+ import math
40
+ import sys
41
+ from copy import deepcopy
42
+ from typing import List, Dict, Tuple, Optional
43
+
44
+ from .constants import ACS_BOND_LENGTH
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Constants
49
+ # ---------------------------------------------------------------------------
50
+
51
+ ACS_BOND_LENGTH_PT = ACS_BOND_LENGTH # target average bond length in points (1 pt = 1/72 in)
52
+
53
+ # Periodic table: element symbol -> (atomic number, default valence)
54
+ # Only the elements we are likely to encounter in medicinal chemistry
55
+ ELEMENT_DATA: Dict[str, Tuple[int, int]] = {
56
+ "H": (1, 1),
57
+ "C": (6, 4),
58
+ "N": (7, 3),
59
+ "O": (8, 2),
60
+ "F": (9, 1),
61
+ "P": (15, 3),
62
+ "S": (16, 2),
63
+ "Cl": (17, 1),
64
+ "Br": (35, 1),
65
+ "I": (53, 1),
66
+ "B": (5, 3),
67
+ "Si": (14, 4),
68
+ "Se": (34, 2),
69
+ }
70
+
71
+ ELEMENT_NUMBERS: Dict[str, int] = {sym: data[0] for sym, data in ELEMENT_DATA.items()}
72
+ DEFAULT_VALENCE: Dict[str, int] = {sym: data[1] for sym, data in ELEMENT_DATA.items()}
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Core normalisation logic
77
+ # ---------------------------------------------------------------------------
78
+
79
+ def _average_bond_length(atoms: List[Dict], bonds: List[Dict]) -> float:
80
+ """Return the average Euclidean bond length, or 1.0 if no bonds."""
81
+ if not bonds:
82
+ return 1.0
83
+ atom_xy = {a["index"]: (a["x"], a["y"]) for a in atoms}
84
+ lengths = []
85
+ for b in bonds:
86
+ x1, y1 = atom_xy.get(b["atom1"], (0, 0))
87
+ x2, y2 = atom_xy.get(b["atom2"], (0, 0))
88
+ d = math.hypot(x2 - x1, y2 - y1)
89
+ if d > 1e-6:
90
+ lengths.append(d)
91
+ return sum(lengths) / len(lengths) if lengths else 1.0
92
+
93
+
94
+ def _bounding_box(atoms: List[Dict]) -> Tuple[float, float, float, float]:
95
+ """Return (min_x, min_y, max_x, max_y) of atom positions."""
96
+ xs = [a["x"] for a in atoms]
97
+ ys = [a["y"] for a in atoms]
98
+ return min(xs), min(ys), max(xs), max(ys)
99
+
100
+
101
+ def strip_explicit_hydrogens(
102
+ atoms: List[Dict],
103
+ bonds: List[Dict],
104
+ ) -> Tuple[List[Dict], List[Dict]]:
105
+ """
106
+ Remove explicit hydrogen atoms from the atom/bond lists.
107
+
108
+ For each heavy atom that had an explicit H neighbour, increment
109
+ num_hydrogens by 1 (or initialise it to 1). Returns new lists;
110
+ input is not modified.
111
+ """
112
+ atoms = deepcopy(atoms)
113
+ bonds = deepcopy(bonds)
114
+
115
+ # Identify explicit H atom indices
116
+ h_indices = {a["index"] for a in atoms if a.get("symbol", "C") == "H"}
117
+ if not h_indices:
118
+ return atoms, bonds
119
+
120
+ # For each H, find the heavy-atom neighbour and bump its H count
121
+ h_count_delta: Dict[int, int] = {}
122
+ bonds_to_remove = set()
123
+ for b in bonds:
124
+ a1, a2 = b["atom1"], b["atom2"]
125
+ if a1 in h_indices or a2 in h_indices:
126
+ bonds_to_remove.add(b["index"])
127
+ heavy = a2 if a1 in h_indices else a1
128
+ h_count_delta[heavy] = h_count_delta.get(heavy, 0) + 1
129
+
130
+ # Update num_hydrogens on heavy atoms
131
+ for a in atoms:
132
+ if a["index"] in h_count_delta:
133
+ a["num_hydrogens"] = a.get("num_hydrogens", 0) + h_count_delta[a["index"]]
134
+
135
+ # Remove H atoms and their bonds
136
+ atoms = [a for a in atoms if a["index"] not in h_indices]
137
+ bonds = [b for b in bonds if b["index"] not in bonds_to_remove]
138
+
139
+ return atoms, bonds
140
+
141
+
142
+ def normalize_coords(
143
+ atoms: List[Dict],
144
+ bonds: List[Dict],
145
+ center_x: float = 200.0,
146
+ center_y: float = 300.0,
147
+ flip_y: bool = True,
148
+ target_bond_length: float = ACS_BOND_LENGTH_PT,
149
+ strip_hydrogens: bool = True,
150
+ ) -> Tuple[List[Dict], List[Dict]]:
151
+ """
152
+ Normalize atom coordinates and return (atoms, bonds) ready for cdxml_builder.
153
+
154
+ Parameters
155
+ ----------
156
+ atoms : list of dicts with keys: index, symbol, x, y (and optionally z,
157
+ num_hydrogens, cfg, etc.)
158
+ bonds : list of dicts with keys: index, order, atom1, atom2 (and optionally cfg)
159
+ center_x, center_y : target centre in CDXML points
160
+ flip_y : True to negate y (converts MOL y-up → CDXML y-down)
161
+ target_bond_length : desired average bond length in points (default 14.40)
162
+ strip_hydrogens : remove explicit H atoms and count them on heavy atoms
163
+
164
+ Returns
165
+ -------
166
+ (atoms, bonds) — new lists, input unchanged
167
+ """
168
+ atoms = deepcopy(atoms)
169
+ bonds = deepcopy(bonds)
170
+
171
+ # Step 1 — strip explicit hydrogens before calculating scale
172
+ if strip_hydrogens:
173
+ atoms, bonds = strip_explicit_hydrogens(atoms, bonds)
174
+
175
+ if not atoms:
176
+ return atoms, bonds
177
+
178
+ # Step 2 — flip y-axis (MOL y-up → CDXML y-down)
179
+ if flip_y:
180
+ for a in atoms:
181
+ a["y"] = -a["y"]
182
+
183
+ # Step 3 — scale to ACS bond length
184
+ avg_bl = _average_bond_length(atoms, bonds)
185
+ if avg_bl > 1e-6 and abs(avg_bl - target_bond_length) > 0.01:
186
+ scale = target_bond_length / avg_bl
187
+ for a in atoms:
188
+ a["x"] *= scale
189
+ a["y"] *= scale
190
+
191
+ # Step 4 — translate so centroid lands at (center_x, center_y)
192
+ xmin, ymin, xmax, ymax = _bounding_box(atoms)
193
+ cx = (xmin + xmax) / 2.0
194
+ cy = (ymin + ymax) / 2.0
195
+ dx = center_x - cx
196
+ dy = center_y - cy
197
+ for a in atoms:
198
+ a["x"] += dx
199
+ a["y"] += dy
200
+
201
+ return atoms, bonds
202
+
203
+
204
+ def normalize_molecule(
205
+ molecule: Dict,
206
+ center_x: float = 200.0,
207
+ center_y: float = 300.0,
208
+ flip_y: bool = True,
209
+ target_bond_length: float = ACS_BOND_LENGTH_PT,
210
+ strip_hydrogens: bool = True,
211
+ ) -> Dict:
212
+ """
213
+ Convenience wrapper: take a molecule dict {"atoms": [...], "bonds": [...]}
214
+ and return a new dict with normalised coordinates.
215
+
216
+ Extra top-level keys (name, role, etc.) are preserved.
217
+ """
218
+ mol = deepcopy(molecule)
219
+ atoms, bonds = normalize_coords(
220
+ mol.get("atoms", []),
221
+ mol.get("bonds", []),
222
+ center_x=center_x,
223
+ center_y=center_y,
224
+ flip_y=flip_y,
225
+ target_bond_length=target_bond_length,
226
+ strip_hydrogens=strip_hydrogens,
227
+ )
228
+ mol["atoms"] = atoms
229
+ mol["bonds"] = bonds
230
+ return mol
231
+
232
+
233
+ def normalize_reaction(
234
+ reactants: List[Dict],
235
+ products: List[Dict],
236
+ reactant_y: float = 300.0,
237
+ product_y: float = 300.0,
238
+ reactant_start_x: float = 50.0,
239
+ product_start_x: float = 350.0,
240
+ molecule_gap: float = 80.0,
241
+ flip_y: bool = True,
242
+ target_bond_length: float = ACS_BOND_LENGTH_PT,
243
+ strip_hydrogens: bool = True,
244
+ ) -> Tuple[List[Dict], List[Dict]]:
245
+ """
246
+ Normalize a set of reactant and product molecules for a reaction scheme.
247
+
248
+ Molecules are laid out horizontally with `molecule_gap` points between
249
+ bounding boxes.
250
+
251
+ Returns (reactants_normalised, products_normalised).
252
+ """
253
+ def layout_molecules(
254
+ mols: List[Dict],
255
+ start_x: float,
256
+ row_y: float,
257
+ ) -> List[Dict]:
258
+ result = []
259
+ cursor_x = start_x
260
+ for mol in mols:
261
+ # First normalise at origin to measure the bounding box
262
+ tmp_atoms, tmp_bonds = normalize_coords(
263
+ mol.get("atoms", []),
264
+ mol.get("bonds", []),
265
+ center_x=0.0,
266
+ center_y=0.0,
267
+ flip_y=flip_y,
268
+ target_bond_length=target_bond_length,
269
+ strip_hydrogens=strip_hydrogens,
270
+ )
271
+ if not tmp_atoms:
272
+ result.append(mol)
273
+ continue
274
+ xmin, _, xmax, _ = _bounding_box(tmp_atoms)
275
+ half_w = (xmax - xmin) / 2.0
276
+ cx = cursor_x + half_w
277
+ # Now normalise for real at the correct position
278
+ norm_atoms, norm_bonds = normalize_coords(
279
+ mol.get("atoms", []),
280
+ mol.get("bonds", []),
281
+ center_x=cx,
282
+ center_y=row_y,
283
+ flip_y=flip_y,
284
+ target_bond_length=target_bond_length,
285
+ strip_hydrogens=strip_hydrogens,
286
+ )
287
+ new_mol = deepcopy(mol)
288
+ new_mol["atoms"] = norm_atoms
289
+ new_mol["bonds"] = norm_bonds
290
+ result.append(new_mol)
291
+ # Move cursor past this molecule's bounding box
292
+ cursor_x = cx + half_w + molecule_gap
293
+ return result
294
+
295
+ norm_reactants = layout_molecules(reactants, reactant_start_x, reactant_y)
296
+ norm_products = layout_molecules(products, product_start_x, product_y)
297
+ return norm_reactants, norm_products
298
+
299
+
300
+ # ---------------------------------------------------------------------------
301
+ # Utility: infer missing num_hydrogens from valence
302
+ # ---------------------------------------------------------------------------
303
+
304
+ def infer_hydrogens(atoms: List[Dict], bonds: List[Dict]) -> List[Dict]:
305
+ """
306
+ For any atom that has no explicit num_hydrogens set, calculate it from
307
+ the default valence minus the sum of bond orders from bonds.
308
+
309
+ This is only called for atoms that don't already have num_hydrogens.
310
+ Returns a new list.
311
+ """
312
+ atoms = deepcopy(atoms)
313
+
314
+ # Count bond-order sum per atom
315
+ bond_order_sum: Dict[int, int] = {}
316
+ for b in bonds:
317
+ o = b.get("order", 1)
318
+ for idx in (b["atom1"], b["atom2"]):
319
+ bond_order_sum[idx] = bond_order_sum.get(idx, 0) + o
320
+
321
+ for a in atoms:
322
+ if "num_hydrogens" in a:
323
+ continue # already explicit
324
+ sym = a.get("symbol", "C")
325
+ if sym == "C":
326
+ continue # carbons get implicit Hs in ChemDraw automatically
327
+ valence = DEFAULT_VALENCE.get(sym)
328
+ if valence is None:
329
+ continue
330
+ used = bond_order_sum.get(a["index"], 0)
331
+ nh = max(0, valence - used)
332
+ a["num_hydrogens"] = nh
333
+
334
+ return atoms
335
+
336
+
337
+ # ---------------------------------------------------------------------------
338
+ # CLI
339
+ # ---------------------------------------------------------------------------
340
+
341
+ def _build_arg_parser() -> argparse.ArgumentParser:
342
+ p = argparse.ArgumentParser(
343
+ description="Normalize atom/bond coordinates to ACS Document 1996 style.",
344
+ formatter_class=argparse.RawDescriptionHelpFormatter,
345
+ )
346
+ p.add_argument("input", help="JSON file with {atoms, bonds} (use - for stdin)")
347
+ p.add_argument(
348
+ "--output", "-o",
349
+ default="-",
350
+ help="Output JSON file (default: stdout)",
351
+ )
352
+ p.add_argument(
353
+ "--center",
354
+ nargs=2,
355
+ type=float,
356
+ metavar=("X", "Y"),
357
+ default=[200.0, 300.0],
358
+ help="Target centre in CDXML points (default: 200 300)",
359
+ )
360
+ p.add_argument(
361
+ "--no-flip-y",
362
+ action="store_true",
363
+ help="Do NOT flip the y-axis (use if coords are already CDXML y-down)",
364
+ )
365
+ p.add_argument(
366
+ "--no-strip-h",
367
+ action="store_true",
368
+ help="Keep explicit hydrogen atoms",
369
+ )
370
+ p.add_argument(
371
+ "--bond-length",
372
+ type=float,
373
+ default=ACS_BOND_LENGTH_PT,
374
+ help=f"Target average bond length in points (default: {ACS_BOND_LENGTH_PT})",
375
+ )
376
+ p.add_argument(
377
+ "--infer-h",
378
+ action="store_true",
379
+ help="Infer missing num_hydrogens from valence after normalisation",
380
+ )
381
+ p.add_argument(
382
+ "--pretty",
383
+ action="store_true",
384
+ help="Pretty-print output JSON",
385
+ )
386
+ return p
387
+
388
+
389
+ def main(argv: Optional[List[str]] = None) -> int:
390
+ parser = _build_arg_parser()
391
+ args = parser.parse_args(argv)
392
+
393
+ # Read input
394
+ if args.input == "-":
395
+ data = json.load(sys.stdin)
396
+ else:
397
+ with open(args.input, encoding="utf-8") as fh:
398
+ data = json.load(fh)
399
+
400
+ atoms = data.get("atoms", [])
401
+ bonds = data.get("bonds", [])
402
+
403
+ if not atoms:
404
+ print("WARNING: no atoms found in input", file=sys.stderr)
405
+
406
+ atoms, bonds = normalize_coords(
407
+ atoms,
408
+ bonds,
409
+ center_x=args.center[0],
410
+ center_y=args.center[1],
411
+ flip_y=not args.no_flip_y,
412
+ target_bond_length=args.bond_length,
413
+ strip_hydrogens=not args.no_strip_h,
414
+ )
415
+
416
+ if args.infer_h:
417
+ atoms = infer_hydrogens(atoms, bonds)
418
+
419
+ # Preserve any extra top-level keys
420
+ out = {k: v for k, v in data.items() if k not in ("atoms", "bonds")}
421
+ out["atoms"] = atoms
422
+ out["bonds"] = bonds
423
+
424
+ indent = 2 if args.pretty else None
425
+ output_text = json.dumps(out, indent=indent)
426
+
427
+ if args.output == "-":
428
+ print(output_text)
429
+ else:
430
+ with open(args.output, "w", encoding="utf-8") as fh:
431
+ fh.write(output_text)
432
+ print(f"Written to {args.output}", file=sys.stderr)
433
+
434
+ return 0
435
+
436
+
437
+ if __name__ == "__main__":
438
+ sys.exit(main())
@@ -0,0 +1,6 @@
1
+ """Deterministic pipeline tools — rigid, multi-step workflows.
2
+
3
+ These modules orchestrate fixed sequences of operations and are not designed
4
+ for flexible agent composition. They remain importable for batch automation
5
+ and backward compatibility.
6
+ """
@@ -0,0 +1,5 @@
1
+ """Legacy polisher pipeline — non-JSON-first CDXML surgery tools.
2
+
3
+ These modules implement the older ELN-export polishing workflow that operates
4
+ directly on CDXML structure, rather than the current JSON-centric pipeline.
5
+ """