sdfrust 0.5.0__tar.gz → 0.6.0__tar.gz

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 (82) hide show
  1. {sdfrust-0.5.0 → sdfrust-0.6.0}/CHANGELOG.md +23 -1
  2. {sdfrust-0.5.0 → sdfrust-0.6.0}/Cargo.lock +4 -4
  3. {sdfrust-0.5.0 → sdfrust-0.6.0}/Cargo.toml +1 -1
  4. {sdfrust-0.5.0 → sdfrust-0.6.0}/PKG-INFO +21 -1
  5. {sdfrust-0.5.0 → sdfrust-0.6.0}/README.md +117 -1
  6. sdfrust-0.6.0/examples/ml_features.rs +233 -0
  7. {sdfrust-0.5.0 → sdfrust-0.6.0}/pyproject.toml +1 -1
  8. {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/Cargo.toml +1 -1
  9. {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/README.md +20 -0
  10. sdfrust-0.6.0/sdfrust-python/examples/README.md +134 -0
  11. sdfrust-0.6.0/sdfrust-python/examples/batch_analysis.py +306 -0
  12. sdfrust-0.6.0/sdfrust-python/examples/data/cholesterol.sdf +268 -0
  13. sdfrust-0.6.0/sdfrust-python/examples/data/dopamine.sdf +159 -0
  14. sdfrust-0.6.0/sdfrust-python/examples/data/drug_library.sdf +1089 -0
  15. sdfrust-0.6.0/sdfrust-python/examples/data/ibuprofen.sdf +182 -0
  16. sdfrust-0.6.0/sdfrust-python/examples/format_conversion.py +266 -0
  17. sdfrust-0.6.0/sdfrust-python/examples/geometry_analysis.py +357 -0
  18. sdfrust-0.6.0/sdfrust-python/examples/ml_features.py +353 -0
  19. {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/src/error.rs +3 -0
  20. {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/src/molecule.rs +510 -0
  21. {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/tests/test_basic.py +1 -1
  22. sdfrust-0.6.0/sdfrust-python/tests/test_ml_validation.py +770 -0
  23. sdfrust-0.6.0/src/descriptors/aromaticity.rs +529 -0
  24. sdfrust-0.6.0/src/descriptors/bond_inference.rs +271 -0
  25. sdfrust-0.6.0/src/descriptors/chirality.rs +888 -0
  26. sdfrust-0.6.0/src/descriptors/conjugation.rs +325 -0
  27. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/descriptors/elements.rs +84 -0
  28. sdfrust-0.6.0/src/descriptors/gasteiger.rs +443 -0
  29. sdfrust-0.6.0/src/descriptors/hybridization.rs +357 -0
  30. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/descriptors/mod.rs +24 -1
  31. sdfrust-0.6.0/src/descriptors/rings.rs +700 -0
  32. sdfrust-0.6.0/src/descriptors/valence.rs +404 -0
  33. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/error.rs +3 -0
  34. sdfrust-0.6.0/src/featurize/mod.rs +10 -0
  35. sdfrust-0.6.0/src/featurize/ogb.rs +431 -0
  36. sdfrust-0.6.0/src/fingerprints/ecfp.rs +386 -0
  37. sdfrust-0.6.0/src/fingerprints/mod.rs +9 -0
  38. sdfrust-0.6.0/src/geometry/angles.rs +389 -0
  39. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/geometry/mod.rs +6 -0
  40. sdfrust-0.6.0/src/geometry/neighbor_list.rs +235 -0
  41. sdfrust-0.6.0/src/graph.rs +250 -0
  42. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/lib.rs +15 -0
  43. sdfrust-0.5.0/sdfrust-python/examples/README.md +0 -82
  44. {sdfrust-0.5.0 → sdfrust-0.6.0}/.gitignore +0 -0
  45. {sdfrust-0.5.0 → sdfrust-0.6.0}/LICENSE +0 -0
  46. {sdfrust-0.5.0 → sdfrust-0.6.0}/examples/basic_usage.rs +0 -0
  47. {sdfrust-0.5.0 → sdfrust-0.6.0}/examples/benchmark.rs +0 -0
  48. {sdfrust-0.5.0 → sdfrust-0.6.0}/examples/parse_molecules.rs +0 -0
  49. {sdfrust-0.5.0 → sdfrust-0.6.0}/python/sdfrust/__init__.py +0 -0
  50. {sdfrust-0.5.0 → sdfrust-0.6.0}/python/sdfrust/py.typed +0 -0
  51. {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/.gitignore +0 -0
  52. {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/LICENSE +0 -0
  53. {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/examples/basic_usage.py +0 -0
  54. {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/src/atom.rs +0 -0
  55. {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/src/bond.rs +0 -0
  56. {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/src/iterators.rs +0 -0
  57. {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/src/lib.rs +0 -0
  58. {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/src/parsing.rs +0 -0
  59. {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/src/writing.rs +0 -0
  60. {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/tests/test_geometry.py +0 -0
  61. {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/tests/test_gzip.py +0 -0
  62. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/atom.rs +0 -0
  63. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/bond.rs +0 -0
  64. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/collection.rs +0 -0
  65. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/descriptors/molecular.rs +0 -0
  66. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/descriptors/topological.rs +0 -0
  67. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/geometry/distance.rs +0 -0
  68. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/geometry/rmsd.rs +0 -0
  69. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/geometry/transform.rs +0 -0
  70. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/molecule.rs +0 -0
  71. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/parser/compression.rs +0 -0
  72. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/parser/mod.rs +0 -0
  73. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/parser/mol2.rs +0 -0
  74. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/parser/sdf.rs +0 -0
  75. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/parser/sdf_v3000.rs +0 -0
  76. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/parser/xyz.rs +0 -0
  77. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/sgroup.rs +0 -0
  78. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/stereogroup.rs +0 -0
  79. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/writer/mod.rs +0 -0
  80. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/writer/mol2.rs +0 -0
  81. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/writer/sdf.rs +0 -0
  82. {sdfrust-0.5.0 → sdfrust-0.6.0}/src/writer/sdf_v3000.rs +0 -0
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] - 2026-02-19
11
+
12
+ ### Added
13
+
14
+ - Graph adjacency infrastructure (`AdjacencyList`) with O(1) neighbor lookups and degree computation
15
+ - Atom valence and hydrogen count: `atom_degree()`, `implicit_hydrogen_count()`, `total_hydrogen_count()` with default valence table for ~20 elements
16
+ - SSSR ring perception via spanning-tree + GF(2) linear independence: `sssr()`, `ring_sizes()`, `smallest_ring_size()`
17
+ - Hybridization inference from bond topology: `atom_hybridization()`, `all_hybridizations()` (S/SP/SP2/SP3/SP3D/SP3D2)
18
+ - Aromaticity perception: two-stage (trust file annotations + Hückel 4n+2 rule on SSSR rings) with support for pyrrole, furan, thiophene
19
+ - Conjugation detection: aromatic bonds, SP2-SP2 single bonds, unsaturated bond chains
20
+ - OGB-compatible GNN featurizer: `ogb_atom_features()` [N, 9], `ogb_bond_features()` [E, 3], `ogb_graph_features()` with directed edge index
21
+ - ECFP/Morgan fingerprints: `ecfp()` bit vector, `ecfp_counts()` hash map, Tanimoto similarity — first pure-Rust implementation
22
+ - Gasteiger-Marsili partial charges: PEOE iterative algorithm with ~15 atom type parameters
23
+ - CIP chirality perception: `atom_chirality()`, `all_chiralities()` with R/S determination from 3D signed volume
24
+ - Bond inference from 3D coordinates: `infer_bonds()`, `infer_bonds_with_config()` using covalent radii
25
+ - Cutoff-based neighbor list for 3D GNNs: `neighbor_list()` with directed edges and distances (geometry feature)
26
+ - Bond angle and dihedral angle computation: `bond_angle()`, `dihedral_angle()`, `all_bond_angles()`, `all_dihedral_angles()` (geometry feature)
27
+ - Python bindings for all new ML features with NumPy array outputs (`get_ogb_atom_features_array()`, `get_ecfp_array()`, `get_gasteiger_charges_array()`)
28
+ - Python examples: format conversion, batch analysis, geometry operations, ML features
29
+ - Rust example: `examples/ml_features.rs`
30
+
10
31
  ## [0.5.0] - 2026-02-17
11
32
 
12
33
  ### Added
@@ -54,7 +75,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
54
75
  - Molecule methods: `formula()`, `centroid()`, `translate()`, `neighbors()`, `element_counts()`
55
76
  - Memory-efficient streaming iterator for large multi-molecule files
56
77
 
57
- [Unreleased]: https://github.com/HFooladi/sdfrust/compare/v0.5.0...HEAD
78
+ [Unreleased]: https://github.com/HFooladi/sdfrust/compare/v0.6.0...HEAD
79
+ [0.6.0]: https://github.com/HFooladi/sdfrust/compare/v0.5.0...v0.6.0
58
80
  [0.5.0]: https://github.com/HFooladi/sdfrust/compare/v0.4.0...v0.5.0
59
81
  [0.4.0]: https://github.com/HFooladi/sdfrust/compare/v0.3.0...v0.4.0
60
82
  [0.3.0]: https://github.com/HFooladi/sdfrust/compare/v0.2.0...v0.3.0
@@ -58,9 +58,9 @@ checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
58
58
 
59
59
  [[package]]
60
60
  name = "bumpalo"
61
- version = "3.19.1"
61
+ version = "3.20.1"
62
62
  source = "registry+https://github.com/rust-lang/crates.io-index"
63
- checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
63
+ checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4"
64
64
 
65
65
  [[package]]
66
66
  name = "bytemuck"
@@ -827,7 +827,7 @@ dependencies = [
827
827
 
828
828
  [[package]]
829
829
  name = "sdfrust"
830
- version = "0.5.0"
830
+ version = "0.6.0"
831
831
  dependencies = [
832
832
  "criterion",
833
833
  "flate2",
@@ -839,7 +839,7 @@ dependencies = [
839
839
 
840
840
  [[package]]
841
841
  name = "sdfrust-python"
842
- version = "0.5.0"
842
+ version = "0.6.0"
843
843
  dependencies = [
844
844
  "numpy",
845
845
  "pyo3",
@@ -4,7 +4,7 @@ resolver = "2"
4
4
 
5
5
  [package]
6
6
  name = "sdfrust"
7
- version = "0.5.0"
7
+ version = "0.6.0"
8
8
  edition = "2024"
9
9
  rust-version = "1.85"
10
10
  authors = ["Hosein Fooladi"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdfrust
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -237,6 +237,26 @@ mol.add_bond(sdfrust.Bond(0, 2, sdfrust.BondOrder.single()))
237
237
  sdfrust.write_sdf_file(mol, "water.sdf")
238
238
  ```
239
239
 
240
+ ## Examples
241
+
242
+ The `examples/` directory contains runnable scripts demonstrating real-world usage:
243
+
244
+ | Script | Description |
245
+ |--------|-------------|
246
+ | [`basic_usage.py`](examples/basic_usage.py) | Core API: parsing, writing, atoms, bonds, descriptors, NumPy |
247
+ | [`format_conversion.py`](examples/format_conversion.py) | Multi-format detection, XYZ parsing, SDF/MOL2 conversion, round-trips |
248
+ | [`batch_analysis.py`](examples/batch_analysis.py) | Drug library processing: filtering, sorting, Lipinski analysis |
249
+ | [`geometry_analysis.py`](examples/geometry_analysis.py) | 3D geometry: distance matrices, RMSD, rotation, transforms |
250
+
251
+ ```bash
252
+ cd sdfrust-python
253
+ maturin develop --features numpy,geometry
254
+ python examples/basic_usage.py
255
+ python examples/format_conversion.py
256
+ python examples/batch_analysis.py
257
+ python examples/geometry_analysis.py
258
+ ```
259
+
240
260
  ## Performance
241
261
 
242
262
  sdfrust is implemented in Rust for maximum performance. Benchmarks show it is
@@ -16,6 +16,9 @@ A fast, pure-Rust parser for SDF (Structure Data File), MOL2, and XYZ chemical s
16
16
  - **Python Bindings**: First-class Python API with NumPy integration
17
17
  - **Streaming Parsing**: Memory-efficient iteration over large files
18
18
  - **Molecular Descriptors**: MW, exact mass, ring count, rotatable bonds, and more
19
+ - **ML-Ready Featurization**: OGB-compatible GNN features, ECFP fingerprints, Gasteiger charges
20
+ - **Chemical Perception**: SSSR rings, aromaticity (Hückel 4n+2), hybridization, conjugation
21
+ - **3D GNN Support**: Cutoff-based neighbor lists, bond/dihedral angles for SchNet/DimeNet/GemNet
19
22
  - **High Performance**: ~220,000 molecules/sec (4-7x faster than RDKit)
20
23
  - **Real-World Validated**: 100% success rate on PDBbind 2024 dataset (27,670 ligand SDF files)
21
24
 
@@ -27,7 +30,7 @@ Add to your `Cargo.toml`:
27
30
 
28
31
  ```toml
29
32
  [dependencies]
30
- sdfrust = "0.5"
33
+ sdfrust = "0.6"
31
34
  ```
32
35
 
33
36
  ### Python
@@ -198,6 +201,90 @@ mol.atoms_by_element("C")
198
201
  mol.bonds_by_order(BondOrder::Double)
199
202
  ```
200
203
 
204
+ ## ML-Ready Features
205
+
206
+ sdfrust provides chemical perception and featurization for molecular ML — compute features in Rust, output NumPy arrays for PyTorch/JAX.
207
+
208
+ ### OGB GNN Featurization
209
+
210
+ ```rust
211
+ use sdfrust::featurize::ogb;
212
+
213
+ let graph = ogb::ogb_graph_features(&mol);
214
+ // graph.atom_features: [N, 9] - atomic_num, chirality, degree, charge, num_hs, radical, hybridization, aromatic, in_ring
215
+ // graph.bond_features: [2E, 3] - bond_type, stereo, conjugated
216
+ // graph.edge_src, graph.edge_dst: directed edge index
217
+ ```
218
+
219
+ ```python
220
+ # Python: direct NumPy arrays for PyTorch Geometric
221
+ atom_feats = mol.get_ogb_atom_features_array() # np.array [N, 9]
222
+ bond_feats = mol.get_ogb_bond_features_array() # np.array [E, 3]
223
+ graph = mol.ogb_graph_features() # dict with edge_src, edge_dst
224
+ ```
225
+
226
+ ### ECFP/Morgan Fingerprints
227
+
228
+ ```rust
229
+ use sdfrust::fingerprints::ecfp;
230
+
231
+ let fp = ecfp::ecfp(&mol, 2, 2048); // ECFP4, 2048 bits
232
+ let similarity = fp.tanimoto(&other_fp);
233
+ ```
234
+
235
+ ```python
236
+ fp = mol.ecfp(radius=2, n_bits=2048) # List[bool]
237
+ fp_array = mol.get_ecfp_array() # np.array (2048,)
238
+ sim = mol.tanimoto_similarity(other_mol) # float
239
+ ```
240
+
241
+ ### Gasteiger Partial Charges
242
+
243
+ ```rust
244
+ use sdfrust::descriptors::gasteiger_charges;
245
+
246
+ let charges = gasteiger_charges(&mol); // Vec<f64>
247
+ ```
248
+
249
+ ```python
250
+ charges = mol.gasteiger_charges() # List[float]
251
+ charges_array = mol.get_gasteiger_charges_array() # np.array (N,)
252
+ ```
253
+
254
+ ### Chemical Perception
255
+
256
+ ```rust
257
+ use sdfrust::descriptors::{sssr, all_aromatic_atoms, all_hybridizations, all_conjugated_bonds};
258
+
259
+ let rings = sssr(&mol); // SSSR ring perception
260
+ let aromatic = all_aromatic_atoms(&mol); // Hückel 4n+2 aromaticity
261
+ let hybs = all_hybridizations(&mol); // SP/SP2/SP3
262
+ let conjugated = all_conjugated_bonds(&mol); // Bond conjugation
263
+ ```
264
+
265
+ ```python
266
+ rings = mol.sssr() # List of ring atom lists
267
+ aromatic = mol.all_aromatic_atoms() # List[bool]
268
+ hybs = mol.all_hybridizations() # List[str]
269
+ conj = mol.all_conjugated_bonds() # List[bool]
270
+ ```
271
+
272
+ ### 3D GNN Features (geometry feature)
273
+
274
+ ```rust
275
+ use sdfrust::geometry::{neighbor_list, all_bond_angles, all_dihedral_angles};
276
+
277
+ let nl = neighbor_list(&mol, 5.0); // Cutoff-based neighbor list
278
+ let angles = all_bond_angles(&mol); // Bond angles for DimeNet
279
+ let dihedrals = all_dihedral_angles(&mol); // Dihedral angles for GemNet
280
+ ```
281
+
282
+ ```python
283
+ nl = mol.neighbor_list(cutoff=5.0) # dict: edge_src, edge_dst, distances
284
+ angles = mol.all_bond_angles() # dict: triplets, angles
285
+ dihedrals = mol.all_dihedral_angles() # dict: quadruplets, angles
286
+ ```
287
+
201
288
  ## Supported Formats
202
289
 
203
290
  | Format | Read | Write |
@@ -262,6 +349,35 @@ sdfrust.write_sdf_auto_file(mol, path) # Auto-select V2000/V3000
262
349
  sdfrust.write_sdf_file_multi(mols, path)
263
350
  ```
264
351
 
352
+ ### ML Feature Methods (on Molecule)
353
+
354
+ ```python
355
+ # Chemical perception
356
+ mol.sssr() # SSSR rings
357
+ mol.all_aromatic_atoms() # Aromaticity
358
+ mol.all_hybridizations() # Hybridization
359
+ mol.all_conjugated_bonds() # Conjugation
360
+
361
+ # ML features
362
+ mol.ogb_atom_features() # [N, 9] OGB features
363
+ mol.ogb_bond_features() # [E, 3] OGB features
364
+ mol.ogb_graph_features() # Full graph dict
365
+ mol.ecfp(radius=2, n_bits=2048) # ECFP bit vector
366
+ mol.tanimoto_similarity(other) # Tanimoto coefficient
367
+ mol.gasteiger_charges() # Partial charges
368
+
369
+ # NumPy arrays (with numpy feature)
370
+ mol.get_ogb_atom_features_array() # np.array [N, 9]
371
+ mol.get_ogb_bond_features_array() # np.array [E, 3]
372
+ mol.get_ecfp_array() # np.array (n_bits,)
373
+ mol.get_gasteiger_charges_array() # np.array (N,)
374
+
375
+ # 3D features (with geometry feature)
376
+ mol.neighbor_list(cutoff=5.0) # Neighbor list dict
377
+ mol.bond_angle(i, j, k) # Angle in radians
378
+ mol.dihedral_angle(i, j, k, l) # Dihedral in radians
379
+ ```
380
+
265
381
  ### Classes
266
382
 
267
383
  - `Molecule`: Main molecular container
@@ -0,0 +1,233 @@
1
+ //! ML-ready feature computation example for sdfrust
2
+ //!
3
+ //! Demonstrates:
4
+ //! - OGB-compatible GNN atom and bond featurization
5
+ //! - ECFP/Morgan fingerprints and Tanimoto similarity
6
+ //! - Gasteiger partial charges
7
+ //! - Ring perception (SSSR), aromaticity, hybridization, conjugation
8
+ //! - Valence and hydrogen count
9
+ //!
10
+ //! Run with: cargo run --example ml_features
11
+
12
+ use sdfrust::descriptors::{
13
+ all_aromatic_atoms, all_aromatic_bonds, all_conjugated_bonds, all_hybridizations,
14
+ all_total_hydrogen_counts, gasteiger_charges, sssr,
15
+ };
16
+ use sdfrust::featurize::ogb;
17
+ use sdfrust::fingerprints::ecfp;
18
+ use sdfrust::graph::AdjacencyList;
19
+ use sdfrust::{Atom, Bond, BondOrder, Molecule};
20
+
21
+ fn make_benzene() -> Molecule {
22
+ let mut mol = Molecule::new("benzene");
23
+ let coords: [(f64, f64); 6] = [
24
+ (1.21, 0.70),
25
+ (1.21, -0.70),
26
+ (0.00, -1.40),
27
+ (-1.21, -0.70),
28
+ (-1.21, 0.70),
29
+ (0.00, 1.40),
30
+ ];
31
+ for (i, &(x, y)) in coords.iter().enumerate() {
32
+ mol.atoms.push(Atom::new(i, "C", x, y, 0.0));
33
+ }
34
+ for i in 0..6 {
35
+ mol.bonds
36
+ .push(Bond::new(i, (i + 1) % 6, BondOrder::Aromatic));
37
+ }
38
+ mol
39
+ }
40
+
41
+ fn make_ethanol() -> Molecule {
42
+ let mut mol = Molecule::new("ethanol");
43
+ mol.atoms.push(Atom::new(0, "C", -0.001, 1.086, 0.008));
44
+ mol.atoms.push(Atom::new(1, "C", 0.002, -0.422, 0.002));
45
+ mol.atoms.push(Atom::new(2, "O", 1.210, -0.907, -0.003));
46
+ mol.atoms.push(Atom::new(3, "H", 1.020, 1.465, 0.001));
47
+ mol.atoms.push(Atom::new(4, "H", -0.536, 1.449, -0.873));
48
+ mol.atoms.push(Atom::new(5, "H", -0.523, 1.437, 0.902));
49
+ mol.atoms.push(Atom::new(6, "H", -0.524, -0.785, 0.891));
50
+ mol.atoms.push(Atom::new(7, "H", -0.510, -0.793, -0.884));
51
+ mol.atoms.push(Atom::new(8, "H", 1.192, -1.870, -0.003));
52
+ mol.bonds.push(Bond::new(0, 1, BondOrder::Single));
53
+ mol.bonds.push(Bond::new(1, 2, BondOrder::Single));
54
+ mol.bonds.push(Bond::new(0, 3, BondOrder::Single));
55
+ mol.bonds.push(Bond::new(0, 4, BondOrder::Single));
56
+ mol.bonds.push(Bond::new(0, 5, BondOrder::Single));
57
+ mol.bonds.push(Bond::new(1, 6, BondOrder::Single));
58
+ mol.bonds.push(Bond::new(1, 7, BondOrder::Single));
59
+ mol.bonds.push(Bond::new(2, 8, BondOrder::Single));
60
+ mol
61
+ }
62
+
63
+ fn make_aspirin() -> Molecule {
64
+ // Simplified aspirin (just the ring + functional groups, no H)
65
+ let mut mol = Molecule::new("aspirin");
66
+ // Benzene ring: C0-C5
67
+ for i in 0..6 {
68
+ mol.atoms.push(Atom::new(i, "C", 0.0, 0.0, 0.0));
69
+ }
70
+ for i in 0..6 {
71
+ let order = if i % 2 == 0 {
72
+ BondOrder::Double
73
+ } else {
74
+ BondOrder::Single
75
+ };
76
+ mol.bonds.push(Bond::new(i, (i + 1) % 6, order));
77
+ }
78
+ // Carboxylic acid: C6(=O7)-O8
79
+ mol.atoms.push(Atom::new(6, "C", 0.0, 0.0, 0.0));
80
+ mol.atoms.push(Atom::new(7, "O", 0.0, 0.0, 0.0));
81
+ mol.atoms.push(Atom::new(8, "O", 0.0, 0.0, 0.0));
82
+ mol.bonds.push(Bond::new(0, 6, BondOrder::Single));
83
+ mol.bonds.push(Bond::new(6, 7, BondOrder::Double));
84
+ mol.bonds.push(Bond::new(6, 8, BondOrder::Single));
85
+ // Ester: O9-C10(=O11)-C12
86
+ mol.atoms.push(Atom::new(9, "O", 0.0, 0.0, 0.0));
87
+ mol.atoms.push(Atom::new(10, "C", 0.0, 0.0, 0.0));
88
+ mol.atoms.push(Atom::new(11, "O", 0.0, 0.0, 0.0));
89
+ mol.atoms.push(Atom::new(12, "C", 0.0, 0.0, 0.0));
90
+ mol.bonds.push(Bond::new(3, 9, BondOrder::Single));
91
+ mol.bonds.push(Bond::new(9, 10, BondOrder::Single));
92
+ mol.bonds.push(Bond::new(10, 11, BondOrder::Double));
93
+ mol.bonds.push(Bond::new(10, 12, BondOrder::Single));
94
+ mol
95
+ }
96
+
97
+ fn main() {
98
+ println!("=== sdfrust ML Features Example ===\n");
99
+
100
+ // --- 1. Ring Perception & Aromaticity ---
101
+ println!("--- Ring Perception & Aromaticity ---\n");
102
+ let benzene = make_benzene();
103
+ let rings = sssr(&benzene);
104
+ println!("Benzene SSSR: {} ring(s)", rings.len());
105
+ for (i, ring) in rings.iter().enumerate() {
106
+ println!(
107
+ " Ring {}: atoms {:?} (size {})",
108
+ i,
109
+ ring.atoms,
110
+ ring.atoms.len()
111
+ );
112
+ }
113
+
114
+ let aromatic_atoms = all_aromatic_atoms(&benzene);
115
+ println!("Aromatic atoms: {:?}", aromatic_atoms);
116
+
117
+ let aromatic_bonds = all_aromatic_bonds(&benzene);
118
+ println!("Aromatic bonds: {:?}", aromatic_bonds);
119
+ println!();
120
+
121
+ // --- 2. Hybridization ---
122
+ println!("--- Hybridization ---\n");
123
+ let ethanol = make_ethanol();
124
+ let hybs = all_hybridizations(&ethanol);
125
+ for (i, hyb) in hybs.iter().enumerate() {
126
+ let elem = &ethanol.atoms[i].element;
127
+ println!(" Atom {} ({}): {:?}", i, elem, hyb);
128
+ }
129
+ println!();
130
+
131
+ // --- 3. Conjugation ---
132
+ println!("--- Conjugation ---\n");
133
+ let aspirin = make_aspirin();
134
+ let conjugated = all_conjugated_bonds(&aspirin);
135
+ for (i, &conj) in conjugated.iter().enumerate() {
136
+ let bond = &aspirin.bonds[i];
137
+ println!(
138
+ " Bond {} ({}-{}): conjugated={}",
139
+ i, bond.atom1, bond.atom2, conj
140
+ );
141
+ }
142
+ println!();
143
+
144
+ // --- 4. Hydrogen Counts ---
145
+ println!("--- Hydrogen Counts ---\n");
146
+ let h_counts = all_total_hydrogen_counts(&ethanol);
147
+ for (i, &count) in h_counts.iter().enumerate() {
148
+ let elem = &ethanol.atoms[i].element;
149
+ println!(" Atom {} ({}): {} total H", i, elem, count);
150
+ }
151
+ println!();
152
+
153
+ // --- 5. OGB GNN Features ---
154
+ println!("--- OGB GNN Features ---\n");
155
+ let atom_feats = ogb::ogb_atom_features(&benzene);
156
+ println!("Benzene atom features [N={}, F=9]:", atom_feats.num_atoms);
157
+ for (i, feat) in atom_feats.features.iter().enumerate() {
158
+ println!(" Atom {}: {:?}", i, feat);
159
+ }
160
+
161
+ let bond_feats = ogb::ogb_bond_features(&benzene);
162
+ println!("\nBenzene bond features [E={}, F=3]:", bond_feats.num_bonds);
163
+ for (i, feat) in bond_feats.features.iter().enumerate() {
164
+ println!(" Bond {}: {:?}", i, feat);
165
+ }
166
+
167
+ let graph = ogb::ogb_graph_features(&benzene);
168
+ println!(
169
+ "\nFull graph: {} atoms, {} directed edges",
170
+ graph.atom_features.num_atoms,
171
+ graph.edge_src.len()
172
+ );
173
+ println!();
174
+
175
+ // --- 6. ECFP Fingerprints ---
176
+ println!("--- ECFP Fingerprints ---\n");
177
+ let fp_benzene = ecfp::ecfp(&benzene, 2, 2048);
178
+ let fp_ethanol = ecfp::ecfp(&ethanol, 2, 2048);
179
+ println!(
180
+ "Benzene ECFP4: {} bits set / {}",
181
+ fp_benzene.num_on_bits(),
182
+ fp_benzene.n_bits
183
+ );
184
+ println!(
185
+ "Ethanol ECFP4: {} bits set / {}",
186
+ fp_ethanol.num_on_bits(),
187
+ fp_ethanol.n_bits
188
+ );
189
+ println!("Benzene density: {:.4}", fp_benzene.density());
190
+ println!(
191
+ "Tanimoto(benzene, ethanol): {:.4}",
192
+ fp_benzene.tanimoto(&fp_ethanol)
193
+ );
194
+ println!(
195
+ "Tanimoto(benzene, benzene): {:.4}",
196
+ fp_benzene.tanimoto(&fp_benzene)
197
+ );
198
+
199
+ // Count fingerprint
200
+ let counts = ecfp::ecfp_counts(&benzene, 2);
201
+ println!(
202
+ "\nBenzene ECFP4 count fingerprint: {} unique features",
203
+ counts.counts.len()
204
+ );
205
+ println!();
206
+
207
+ // --- 7. Gasteiger Charges ---
208
+ println!("--- Gasteiger Charges ---\n");
209
+ let charges = gasteiger_charges(&ethanol);
210
+ for (i, &charge) in charges.iter().enumerate() {
211
+ let elem = &ethanol.atoms[i].element;
212
+ println!(" Atom {} ({}): charge = {:+.4}", i, elem, charge);
213
+ }
214
+ println!();
215
+
216
+ // --- 8. Graph Adjacency ---
217
+ println!("--- Graph Adjacency ---\n");
218
+ let adj = AdjacencyList::from_molecule(&ethanol);
219
+ for i in 0..3 {
220
+ let elem = &ethanol.atoms[i].element;
221
+ let neighbors = adj.neighbor_atoms(i);
222
+ println!(
223
+ " Atom {} ({}): degree={}, heavy_degree={}, neighbors={:?}",
224
+ i,
225
+ elem,
226
+ adj.degree(i),
227
+ adj.heavy_degree(i),
228
+ neighbors
229
+ );
230
+ }
231
+
232
+ println!("\n=== Example Complete ===");
233
+ }
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "sdfrust"
7
- version = "0.5.0"
7
+ version = "0.6.0"
8
8
  description = "Fast Rust-based SDF, MOL2, and XYZ molecular structure file parser"
9
9
  authors = [
10
10
  {name = "Hosein Fooladi", email = "hosein@example.com"}
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sdfrust-python"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  edition = "2021"
5
5
  rust-version = "1.70"
6
6
  authors = ["Hosein Fooladi"]
@@ -206,6 +206,26 @@ mol.add_bond(sdfrust.Bond(0, 2, sdfrust.BondOrder.single()))
206
206
  sdfrust.write_sdf_file(mol, "water.sdf")
207
207
  ```
208
208
 
209
+ ## Examples
210
+
211
+ The `examples/` directory contains runnable scripts demonstrating real-world usage:
212
+
213
+ | Script | Description |
214
+ |--------|-------------|
215
+ | [`basic_usage.py`](examples/basic_usage.py) | Core API: parsing, writing, atoms, bonds, descriptors, NumPy |
216
+ | [`format_conversion.py`](examples/format_conversion.py) | Multi-format detection, XYZ parsing, SDF/MOL2 conversion, round-trips |
217
+ | [`batch_analysis.py`](examples/batch_analysis.py) | Drug library processing: filtering, sorting, Lipinski analysis |
218
+ | [`geometry_analysis.py`](examples/geometry_analysis.py) | 3D geometry: distance matrices, RMSD, rotation, transforms |
219
+
220
+ ```bash
221
+ cd sdfrust-python
222
+ maturin develop --features numpy,geometry
223
+ python examples/basic_usage.py
224
+ python examples/format_conversion.py
225
+ python examples/batch_analysis.py
226
+ python examples/geometry_analysis.py
227
+ ```
228
+
209
229
  ## Performance
210
230
 
211
231
  sdfrust is implemented in Rust for maximum performance. Benchmarks show it is
@@ -0,0 +1,134 @@
1
+ # sdfrust Python Examples
2
+
3
+ This directory contains example scripts demonstrating the Python API.
4
+
5
+ ## Setup
6
+
7
+ Before running examples, build and install the Python bindings:
8
+
9
+ ```bash
10
+ cd sdfrust-python
11
+ uv venv .venv --python 3.11
12
+ source .venv/bin/activate
13
+ uv pip install maturin numpy pytest
14
+ maturin develop --features numpy
15
+ ```
16
+
17
+ ## Running Examples
18
+
19
+ ```bash
20
+ # From sdfrust-python directory
21
+ python examples/basic_usage.py
22
+ python examples/format_conversion.py
23
+ python examples/batch_analysis.py
24
+
25
+ # Geometry examples require the geometry feature
26
+ maturin develop --features numpy,geometry
27
+ python examples/geometry_analysis.py
28
+ ```
29
+
30
+ ## Examples Overview
31
+
32
+ ### basic_usage.py
33
+
34
+ Core functionality covering:
35
+
36
+ - **Parsing SDF files**: Load molecules from V2000 and V3000 SDF files
37
+ - **Parsing SDF strings**: Parse inline SDF content
38
+ - **Creating molecules**: Build molecules programmatically with atoms and bonds
39
+ - **Atom access**: Read atom properties, filter by element, calculate distances
40
+ - **Bond access**: Work with bond orders, stereochemistry, and connectivity
41
+ - **Molecular descriptors**: Calculate MW, exact mass, ring count, rotatable bonds
42
+ - **Geometry operations**: Centroid calculation, translation, centering
43
+ - **Properties**: Read/write SDF data block properties
44
+ - **Multi-molecule files**: Iterate over files with multiple molecules
45
+ - **Writing SDF**: Export molecules to V2000 or V3000 format
46
+ - **MOL2 parsing**: Read TRIPOS MOL2 format files
47
+ - **V3000 format**: Work with extended SDF format
48
+ - **NumPy integration**: Get/set coordinates as NumPy arrays
49
+ - **Ring detection**: Identify atoms and bonds in rings
50
+ - **Charged atoms**: Handle formal charges
51
+
52
+ ### format_conversion.py
53
+
54
+ Multi-format detection, parsing, and conversion:
55
+
56
+ - **Format detection**: Identify SDF V2000, V3000, and MOL2 from content (`detect_format()`)
57
+ - **Auto-detection parsing**: Parse any format with one function (`parse_auto_file()`)
58
+ - **XYZ parsing**: Single and multi-molecule XYZ files, streaming with `iter_xyz_file()`
59
+ - **SDF → MOL2 conversion**: Convert molecules with round-trip verification
60
+ - **MOL2 → SDF conversion**: Convert molecules with round-trip verification
61
+ - **Batch conversion**: Write multi-molecule SDF and MOL2 files (`write_sdf_file_multi()`, `write_mol2_file_multi()`)
62
+ - **Gzip support**: Check runtime feature availability (`gzip_enabled()`)
63
+
64
+ ### batch_analysis.py
65
+
66
+ Compound library processing with real drug molecules:
67
+
68
+ - **Library loading**: Parse multi-molecule SDF with summary table
69
+ - **Streaming iteration**: Memory-efficient processing with `iter_sdf_file()`
70
+ - **MW filtering**: Filter by molecular weight range, write subsets
71
+ - **Descriptor sorting**: Rank molecules by MW and heavy atom count
72
+ - **Element composition**: Compare C/H/O/N/S counts across molecules
73
+ - **Bond profiles**: Compare single/double/aromatic bond distributions
74
+ - **Lipinski analysis**: Apply Rule of Five criteria using SDF properties
75
+ - **Finding extremes**: Identify molecules with most atoms, highest MW, most rings
76
+
77
+ ### geometry_analysis.py
78
+
79
+ 3D geometry operations (requires `--features geometry`):
80
+
81
+ - **Distance matrix**: Pairwise distances, closest/farthest atom pairs
82
+ - **Bond length analysis**: Extract actual bond lengths by bond type
83
+ - **RMSD comparison**: Compare identical, translated, and perturbed molecules
84
+ - **Rotation**: Axis-angle rotation with distance preservation verification
85
+ - **Combined transforms**: Rotation matrix + translation
86
+ - **NumPy analysis**: Bounding box, spatial extent, radius of gyration
87
+ - **Conformer comparison**: Perturb coordinates, center, compare RMSD
88
+
89
+ ### Example Data
90
+
91
+ The `data/` directory contains real drug molecules downloaded from PubChem:
92
+
93
+ - `ibuprofen.sdf` — NSAID (CID 3672, 33 atoms)
94
+ - `dopamine.sdf` — Neurotransmitter (CID 681, 22 atoms)
95
+ - `cholesterol.sdf` — Steroid (CID 5997, 74 atoms)
96
+ - `drug_library.sdf` — Combined multi-molecule file (6 drugs)
97
+
98
+ ## Quick Reference
99
+
100
+ ```python
101
+ import sdfrust
102
+
103
+ # Parse files
104
+ mol = sdfrust.parse_sdf_file("molecule.sdf")
105
+ mol = sdfrust.parse_mol2_file("molecule.mol2")
106
+ mol = sdfrust.parse_sdf_auto_file("molecule.sdf") # Auto-detect V2000/V3000
107
+
108
+ # Parse strings
109
+ mol = sdfrust.parse_sdf_string(sdf_content)
110
+ mol = sdfrust.parse_mol2_string(mol2_content)
111
+
112
+ # Create molecule
113
+ mol = sdfrust.Molecule("my_molecule")
114
+ mol.add_atom(sdfrust.Atom(0, "C", 0.0, 0.0, 0.0))
115
+ mol.add_bond(sdfrust.Bond(0, 1, sdfrust.BondOrder.single()))
116
+
117
+ # Basic properties
118
+ print(mol.name, mol.num_atoms, mol.num_bonds)
119
+ print(mol.formula())
120
+ print(mol.molecular_weight())
121
+
122
+ # Iterate large files efficiently
123
+ for mol in sdfrust.iter_sdf_file("large_library.sdf"):
124
+ process(mol)
125
+
126
+ # Write files
127
+ sdfrust.write_sdf_file(mol, "output.sdf")
128
+ sdf_str = sdfrust.write_sdf_string(mol)
129
+
130
+ # NumPy integration
131
+ coords = mol.get_coords_array() # shape (N, 3)
132
+ mol.set_coords_array(new_coords)
133
+ atomic_nums = mol.get_atomic_numbers()
134
+ ```