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.
- {sdfrust-0.5.0 → sdfrust-0.6.0}/CHANGELOG.md +23 -1
- {sdfrust-0.5.0 → sdfrust-0.6.0}/Cargo.lock +4 -4
- {sdfrust-0.5.0 → sdfrust-0.6.0}/Cargo.toml +1 -1
- {sdfrust-0.5.0 → sdfrust-0.6.0}/PKG-INFO +21 -1
- {sdfrust-0.5.0 → sdfrust-0.6.0}/README.md +117 -1
- sdfrust-0.6.0/examples/ml_features.rs +233 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/pyproject.toml +1 -1
- {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/Cargo.toml +1 -1
- {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/README.md +20 -0
- sdfrust-0.6.0/sdfrust-python/examples/README.md +134 -0
- sdfrust-0.6.0/sdfrust-python/examples/batch_analysis.py +306 -0
- sdfrust-0.6.0/sdfrust-python/examples/data/cholesterol.sdf +268 -0
- sdfrust-0.6.0/sdfrust-python/examples/data/dopamine.sdf +159 -0
- sdfrust-0.6.0/sdfrust-python/examples/data/drug_library.sdf +1089 -0
- sdfrust-0.6.0/sdfrust-python/examples/data/ibuprofen.sdf +182 -0
- sdfrust-0.6.0/sdfrust-python/examples/format_conversion.py +266 -0
- sdfrust-0.6.0/sdfrust-python/examples/geometry_analysis.py +357 -0
- sdfrust-0.6.0/sdfrust-python/examples/ml_features.py +353 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/src/error.rs +3 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/src/molecule.rs +510 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/tests/test_basic.py +1 -1
- sdfrust-0.6.0/sdfrust-python/tests/test_ml_validation.py +770 -0
- sdfrust-0.6.0/src/descriptors/aromaticity.rs +529 -0
- sdfrust-0.6.0/src/descriptors/bond_inference.rs +271 -0
- sdfrust-0.6.0/src/descriptors/chirality.rs +888 -0
- sdfrust-0.6.0/src/descriptors/conjugation.rs +325 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/descriptors/elements.rs +84 -0
- sdfrust-0.6.0/src/descriptors/gasteiger.rs +443 -0
- sdfrust-0.6.0/src/descriptors/hybridization.rs +357 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/descriptors/mod.rs +24 -1
- sdfrust-0.6.0/src/descriptors/rings.rs +700 -0
- sdfrust-0.6.0/src/descriptors/valence.rs +404 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/error.rs +3 -0
- sdfrust-0.6.0/src/featurize/mod.rs +10 -0
- sdfrust-0.6.0/src/featurize/ogb.rs +431 -0
- sdfrust-0.6.0/src/fingerprints/ecfp.rs +386 -0
- sdfrust-0.6.0/src/fingerprints/mod.rs +9 -0
- sdfrust-0.6.0/src/geometry/angles.rs +389 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/geometry/mod.rs +6 -0
- sdfrust-0.6.0/src/geometry/neighbor_list.rs +235 -0
- sdfrust-0.6.0/src/graph.rs +250 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/lib.rs +15 -0
- sdfrust-0.5.0/sdfrust-python/examples/README.md +0 -82
- {sdfrust-0.5.0 → sdfrust-0.6.0}/.gitignore +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/LICENSE +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/examples/basic_usage.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/examples/benchmark.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/examples/parse_molecules.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/python/sdfrust/__init__.py +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/python/sdfrust/py.typed +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/.gitignore +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/LICENSE +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/examples/basic_usage.py +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/src/atom.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/src/bond.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/src/iterators.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/src/lib.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/src/parsing.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/src/writing.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/tests/test_geometry.py +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/sdfrust-python/tests/test_gzip.py +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/atom.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/bond.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/collection.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/descriptors/molecular.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/descriptors/topological.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/geometry/distance.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/geometry/rmsd.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/geometry/transform.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/molecule.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/parser/compression.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/parser/mod.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/parser/mol2.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/parser/sdf.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/parser/sdf_v3000.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/parser/xyz.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/sgroup.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/stereogroup.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/writer/mod.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/writer/mol2.rs +0 -0
- {sdfrust-0.5.0 → sdfrust-0.6.0}/src/writer/sdf.rs +0 -0
- {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.
|
|
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.
|
|
61
|
+
version = "3.20.1"
|
|
62
62
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
63
|
-
checksum = "
|
|
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.
|
|
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.
|
|
842
|
+
version = "0.6.0"
|
|
843
843
|
dependencies = [
|
|
844
844
|
"numpy",
|
|
845
845
|
"pyo3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sdfrust
|
|
3
|
-
Version: 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.
|
|
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(ðanol);
|
|
125
|
+
for (i, hyb) in hybs.iter().enumerate() {
|
|
126
|
+
let elem = ðanol.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(ðanol);
|
|
147
|
+
for (i, &count) in h_counts.iter().enumerate() {
|
|
148
|
+
let elem = ðanol.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(ðanol, 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(ðanol);
|
|
210
|
+
for (i, &charge) in charges.iter().enumerate() {
|
|
211
|
+
let elem = ðanol.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(ðanol);
|
|
219
|
+
for i in 0..3 {
|
|
220
|
+
let elem = ðanol.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
|
+
}
|
|
@@ -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
|
+
```
|