sdfrust 0.6.0__tar.gz → 0.7.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 (97) hide show
  1. {sdfrust-0.6.0 → sdfrust-0.7.0}/.gitignore +1 -0
  2. sdfrust-0.7.0/CHANGELOG.md +241 -0
  3. {sdfrust-0.6.0 → sdfrust-0.7.0}/Cargo.lock +185 -357
  4. {sdfrust-0.6.0 → sdfrust-0.7.0}/Cargo.toml +6 -3
  5. {sdfrust-0.6.0 → sdfrust-0.7.0}/PKG-INFO +2 -2
  6. {sdfrust-0.6.0 → sdfrust-0.7.0}/README.md +55 -4
  7. sdfrust-0.7.0/benches/mol2_parse_benchmark.rs +208 -0
  8. sdfrust-0.7.0/benches/roundtrip_benchmark.rs +277 -0
  9. sdfrust-0.7.0/benches/sdf_parse_benchmark.rs +221 -0
  10. {sdfrust-0.6.0 → sdfrust-0.7.0}/pyproject.toml +3 -3
  11. sdfrust-0.7.0/scripts/gen_element_table.py +48 -0
  12. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/Cargo.toml +2 -2
  13. sdfrust-0.7.0/sdfrust-python/benchmarks/compare_tools.py +695 -0
  14. sdfrust-0.7.0/sdfrust-python/benchmarks/reports/after_phase1.md +290 -0
  15. sdfrust-0.7.0/sdfrust-python/benchmarks/reports/after_phase2.md +157 -0
  16. sdfrust-0.7.0/sdfrust-python/benchmarks/reports/baseline_v0.6.0.md +317 -0
  17. sdfrust-0.7.0/sdfrust-python/benchmarks/reports/pdbbind2024_sample2000.md +185 -0
  18. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/src/atom.rs +55 -1
  19. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/src/error.rs +4 -0
  20. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/src/molecule.rs +35 -9
  21. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/tests/test_basic.py +11 -1
  22. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/tests/test_ml_validation.py +96 -167
  23. sdfrust-0.7.0/sdfrust-python/tests/test_regressions.py +65 -0
  24. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/atom.rs +31 -1
  25. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/descriptors/aromaticity.rs +216 -263
  26. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/descriptors/chirality.rs +56 -25
  27. sdfrust-0.7.0/src/descriptors/conjugation.rs +290 -0
  28. sdfrust-0.7.0/src/descriptors/element_table.rs +456 -0
  29. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/descriptors/elements.rs +92 -218
  30. sdfrust-0.7.0/src/descriptors/gasteiger.rs +444 -0
  31. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/descriptors/hybridization.rs +80 -112
  32. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/descriptors/mod.rs +9 -2
  33. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/descriptors/molecular.rs +54 -17
  34. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/descriptors/rings.rs +127 -321
  35. sdfrust-0.7.0/src/descriptors/stereo.rs +214 -0
  36. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/descriptors/topological.rs +97 -73
  37. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/descriptors/valence.rs +65 -87
  38. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/error.rs +3 -0
  39. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/featurize/ogb.rs +142 -58
  40. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/fingerprints/ecfp.rs +173 -94
  41. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/molecule.rs +41 -29
  42. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/parser/compression.rs +5 -5
  43. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/parser/mol2.rs +86 -64
  44. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/parser/sdf.rs +275 -146
  45. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/parser/sdf_v3000.rs +124 -74
  46. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/parser/xyz.rs +17 -29
  47. sdfrust-0.7.0/src/writer/sdf.rs +279 -0
  48. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/writer/sdf_v3000.rs +9 -9
  49. sdfrust-0.6.0/CHANGELOG.md +0 -84
  50. sdfrust-0.6.0/src/descriptors/conjugation.rs +0 -325
  51. sdfrust-0.6.0/src/descriptors/gasteiger.rs +0 -443
  52. sdfrust-0.6.0/src/writer/sdf.rs +0 -178
  53. {sdfrust-0.6.0 → sdfrust-0.7.0}/LICENSE +0 -0
  54. {sdfrust-0.6.0 → sdfrust-0.7.0}/examples/basic_usage.rs +0 -0
  55. {sdfrust-0.6.0 → sdfrust-0.7.0}/examples/benchmark.rs +0 -0
  56. {sdfrust-0.6.0 → sdfrust-0.7.0}/examples/ml_features.rs +0 -0
  57. {sdfrust-0.6.0 → sdfrust-0.7.0}/examples/parse_molecules.rs +0 -0
  58. {sdfrust-0.6.0 → sdfrust-0.7.0}/python/sdfrust/__init__.py +0 -0
  59. {sdfrust-0.6.0 → sdfrust-0.7.0}/python/sdfrust/py.typed +0 -0
  60. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/.gitignore +0 -0
  61. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/LICENSE +0 -0
  62. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/README.md +0 -0
  63. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/examples/README.md +0 -0
  64. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/examples/basic_usage.py +0 -0
  65. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/examples/batch_analysis.py +0 -0
  66. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/examples/data/cholesterol.sdf +0 -0
  67. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/examples/data/dopamine.sdf +0 -0
  68. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/examples/data/drug_library.sdf +0 -0
  69. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/examples/data/ibuprofen.sdf +0 -0
  70. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/examples/format_conversion.py +0 -0
  71. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/examples/geometry_analysis.py +0 -0
  72. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/examples/ml_features.py +0 -0
  73. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/src/bond.rs +0 -0
  74. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/src/iterators.rs +0 -0
  75. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/src/lib.rs +0 -0
  76. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/src/parsing.rs +0 -0
  77. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/src/writing.rs +0 -0
  78. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/tests/test_geometry.py +0 -0
  79. {sdfrust-0.6.0 → sdfrust-0.7.0}/sdfrust-python/tests/test_gzip.py +0 -0
  80. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/bond.rs +0 -0
  81. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/collection.rs +0 -0
  82. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/descriptors/bond_inference.rs +0 -0
  83. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/featurize/mod.rs +0 -0
  84. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/fingerprints/mod.rs +0 -0
  85. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/geometry/angles.rs +0 -0
  86. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/geometry/distance.rs +0 -0
  87. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/geometry/mod.rs +0 -0
  88. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/geometry/neighbor_list.rs +0 -0
  89. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/geometry/rmsd.rs +0 -0
  90. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/geometry/transform.rs +0 -0
  91. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/graph.rs +0 -0
  92. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/lib.rs +0 -0
  93. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/parser/mod.rs +0 -0
  94. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/sgroup.rs +0 -0
  95. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/stereogroup.rs +0 -0
  96. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/writer/mod.rs +0 -0
  97. {sdfrust-0.6.0 → sdfrust-0.7.0}/src/writer/mol2.rs +0 -0
@@ -21,3 +21,4 @@ Thumbs.db
21
21
  benches/data/*.sdf
22
22
  benches/comparison/.venv/
23
23
  benches/comparison/results/
24
+ sdfrust-python/benchmarks/reports/*.json
@@ -0,0 +1,241 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.7.0] - 2026-10-14
11
+
12
+ Correctness release. sdfrust was audited and every feature that has an
13
+ equivalent in an established toolkit was compared against RDKit 2025.09,
14
+ OpenBabel 3.1 and the `ogb` package with
15
+ `sdfrust-python/benchmarks/compare_tools.py`. The comparison used 4,553
16
+ molecules (NCI first 5k, EGFR and CDK2 ligands, heavy-atom-only and explicit-H;
17
+ 113k atoms) plus 2,000 PDBbind 2024 ligands. Each fix has a regression test.
18
+
19
+ Agreement with RDKit before → after:
20
+
21
+ | Feature | 0.6.0 | 0.7.0 |
22
+ |---|---|---|
23
+ | Molecular weight / exact mass / formula | 54% | 100% |
24
+ | Aromatic atoms / bonds | 97.8% / 97.2% | 100% / 100% |
25
+ | Hybridization | 98.8% | >99.99% |
26
+ | Conjugated bonds | 98.4% | >99.99% |
27
+ | Gasteiger charges | 46% | 100% |
28
+ | Rotatable bonds | 68% | 100% |
29
+ | SSSR ring sizes | 99.8% | 100% |
30
+ | OGB bond stereo | 99.4% | 100% |
31
+ | ECFP bits identical to RDKit Morgan | ~0% | 100% |
32
+
33
+ Parser/writer edge cases (V3000 via V2000 entry points, charge/isotope/radical
34
+ round-trips, malformed records, non-ASCII input, ...): 4/17 → 17/17. OGB
35
+ features against `ogb.utils.features`: atomic number 0% → 100%, hybridization
36
+ 0.9% → >99.99%.
37
+
38
+ Also checked on 2,000 PDBbind 2024 ligands (3D, explicit H; 123k atoms):
39
+ every property agrees with RDKit at ≥99.9%. All 27,670 PDBbind 2024 ligand
40
+ files still parse.
41
+ - The only ECFP or formal-charge differences are two N-oxides that RDKit
42
+ rewrites while sanitizing (N=O becomes [n+][O-]); sdfrust keeps the charges
43
+ as written in the file.
44
+ - 0.08% of chiral tags differ, on RDKit's ring-stereo centers (dependent
45
+ stereocenters across rings), which aren't perceived yet.
46
+
47
+ ### Changed (breaking): file formats and OGB encoding
48
+
49
+ - **OGB features now use the real `ogb.utils.features` encoding.** Values were
50
+ previously raw values on a different scale, which is incompatible with OGB's
51
+ `AtomEncoder`/`BondEncoder`. Models trained on 0.6 features must be retrained.
52
+ - atomic number is `Z - 1` (misc = 118)
53
+ - hybridization is `[SP, SP2, SP3, SP3D, SP3D2, misc]` (S → misc)
54
+ - degree is total degree including implicit H
55
+ - radicals are counted as electrons
56
+ - bond stereo uses `[NONE, Z, E, CIS, TRANS, ANY]` (wedge flags are no longer read as E/Z)
57
+ - out-of-range values go to the `misc` bucket
58
+ - `Hybridization::to_ogb_index()` returns OGB indices (SP = 0 … SP3D2 = 4, S/Other = 5).
59
+ - MOL2: bond type `am` (amide) is now `Single`, not `Aromatic`.
60
+ - MOL2: partial charges are stored in the new `Atom::partial_charge` instead of
61
+ being rounded into `formal_charge`. `N.4` atoms get formal charge +1.
62
+ - Isotopes from `M ISO` / V3000 `MASS=` are stored as absolute mass numbers in the
63
+ new `Atom::isotope: Option<u16>`. Previously they went into `mass_difference: i8`,
64
+ which also corrupted masses ≥ 128. `mass_difference` now only holds the V2000
65
+ atom-block column.
66
+ - `write_sdf` (V2000) returns `SdfError::Unwritable` for molecules it cannot
67
+ represent: more than 999 atoms/bonds, coordinates beyond ±9999.9999, or long atom
68
+ symbols. It previously wrote a corrupt file. `write_sdf_auto` falls back to V3000
69
+ in these cases.
70
+ - Writers return `SdfError::InvalidAtomIndex` for bonds or groups that reference
71
+ missing atoms. They previously panicked or wrote invalid files.
72
+ - `MaybeGzReader::Gzip` wraps `MultiGzDecoder`.
73
+
74
+ ### Changed (breaking): chemistry perception (values change for the same input)
75
+
76
+ - `molecular_weight`, `exact_mass` and `Molecule::formula` count implicit
77
+ hydrogens, so heavy-atom-only files give the full molecule. They also use
78
+ isotope masses for labelled atoms, and exact mass corrects for the electron
79
+ mass of ions. Formulas use RDKit's style: C, H, then alphabetical, with a
80
+ charge suffix (`C2H3O2-`). A molecule built from atoms without bonds now
81
+ gets implicit H on every atom; add the bonds, or set `Atom::no_implicit`.
82
+ - Implicit hydrogens follow RDKit's valence model: a charged atom takes the
83
+ valences of its isoelectronic neutral element (N⁺ like C, B⁻² like N).
84
+ Radicals and the molfile valence field are honoured.
85
+ - Protonated/quaternary N⁺ and sulfoxide/sulfonium S can be tetrahedral
86
+ stereocenters (the lone pair counts as the fourth substituent).
87
+ - Aromaticity uses per-atom π-electron counts and fused-ring unions (RDKit's
88
+ model). Kekulé naphthalene, indole, azulene, purines, pyridones and
89
+ tropylium are recognized; sulfoxide rings are not.
90
+ - Hybridization comes from σ bonds plus lone pairs, and conjugation follows
91
+ RDKit's rules. `atom_hybridization` and `all_hybridizations` now agree.
92
+ - Gasteiger charges are a port of RDKit's implementation: same parameter
93
+ table, 12 iterations, implicit H as virtual atoms, H χ⁺ = 20.02, and formal
94
+ charge split over conjugated atoms. Atoms without parameters keep their
95
+ formal charge.
96
+ - `rotatable_bond_count` uses RDKit's default (strict) definition, which
97
+ excludes amide/ester C–X bonds, CF3/CCl3/CBr3, tert-butyl and bonds next
98
+ to triple bonds. Aspirin now has 2 rotatable bonds, not 3.
99
+ - `sssr` returns a true minimum cycle basis (Horton) with deterministic order.
100
+ - ECFP/Morgan fingerprints are bit-identical to RDKit's
101
+ `GetMorganGenerator(radius, fpSize)` with default options.
102
+ - Chiral tags (CW/CCW) are relative to bond order, as in RDKit. Their docs no
103
+ longer call them R/S.
104
+ - OGB bond stereo is perceived from coordinates: Z = 1, E = 2, unknown = 5.
105
+ - The element table covers all 118 elements, with isotope masses. MOL2 and
106
+ XYZ atoms are flagged `no_implicit`.
107
+
108
+ ### Added
109
+
110
+ - `Atom::isotope`, `Atom::partial_charge`, `Atom::radical_electrons()`. Python
111
+ `Atom` exposes `isotope`, `partial_charge`, `radical` and `radical_electrons`.
112
+ - `SdfParser::skip_to_record_end()`, `SdfV3000Parser::skip_to_record_end()`.
113
+ - `sdfrust-python/benchmarks/compare_tools.py`: a differential comparison harness
114
+ against RDKit, OpenBabel and OGB, with Markdown/JSON reports.
115
+ - `tests/regression_tests.rs` and `sdfrust-python/tests/test_regressions.py`.
116
+ - The Python OGB validation tests compare against `ogb.utils.features` itself, and
117
+ CI installs RDKit and OGB so they run.
118
+ - `descriptors::stereo::double_bond_stereo`: E/Z from coordinates.
119
+ - `gasteiger_charges_with_hydrogens`: also returns implicit-H charges.
120
+ - `element_by_atomic_number`, `isotope_mass`; `ElementData::outer_electrons`
121
+ and `ElementData::valences`.
122
+ - `Atom::no_implicit`, also exposed in Python.
123
+ - `scripts/gen_element_table.py`, which regenerates the periodic table from RDKit.
124
+ - `compare_tools.py --rdkit-data/--smiles/--extra` for larger corpora, and
125
+ exact ECFP bit comparison.
126
+
127
+ ### Fixed
128
+
129
+ - `parse_sdf_*` / `iter_sdf_file` silently returned an empty molecule for V3000
130
+ records. V3000 records are now detected per record, so mixed V2000/V3000 files
131
+ parse correctly.
132
+ - A trailing blank line after the final `$$$$` made `*_multi` fail on the whole file.
133
+ - Iterators (SDF V2000/V3000, MOL2) now resume at the next record after a malformed
134
+ one, instead of stopping, as the crate docs described.
135
+ - Panics on non-ASCII text in V2000 fixed-width lines. Invalid UTF-8 (e.g. Latin-1
136
+ property values) is now replaced instead of aborting the file.
137
+ - Huge declared atom counts no longer abort with an allocation failure.
138
+ - A missing `M END` no longer swallows the following record(s).
139
+ - V2000 charges: all nonzero charges are written to `M CHG`. The parser lets
140
+ `M CHG`/`M RAD` supersede atom-block charges, as the CTfile spec requires.
141
+ Previously a molecule with a +1 and a +4 charge lost the +1 when read by RDKit.
142
+ - Radicals: atom-block charge code 4 and `M RAD` are parsed and `M RAD` is
143
+ written. Radicals no longer force V3000 output.
144
+ - `M CHG`/`M ISO` were written using `atom.index` instead of the atom's position.
145
+ - Data items now end at a blank line. Property output is sorted by name, so it is
146
+ deterministic.
147
+ - MOL2: non-contiguous atom ids are resolved through an id map; element case is
148
+ normalized (`CL` → `Cl`); `NO_CHARGES` is honored.
149
+ - XYZ: atomic numbers 1–118 are supported (previously ~25 elements).
150
+ - `ecfp(n_bits = 0)` no longer panics (returns an empty fingerprint; Python raises
151
+ `ValueError`). Python `Molecule.add_bond` raises `IndexError` for bad atom indices.
152
+ - Gzip: multi-member (concatenated / bgzip) files are read completely.
153
+ - The V2000 parser and writer handle stereo code 3 ("cis/trans unknown") on
154
+ double bonds.
155
+
156
+ ### Packaging
157
+
158
+ - Python wheels include the `geometry` feature (RMSD, transforms, neighbor
159
+ lists, angles), so the documented geometry API works from PyPI.
160
+ - macOS and Windows wheels are built for Python 3.9–3.13; 0.6.0 shipped only
161
+ 3.12 wheels on those platforms.
162
+ - The Python sdist installs from source again. 0.6.0's failed because the
163
+ crate's `[[bench]]` targets were excluded from the package. CI now builds
164
+ and imports the sdist.
165
+
166
+ ## [0.6.0] - 2026-02-19
167
+
168
+ ### Added
169
+
170
+ - Graph adjacency infrastructure (`AdjacencyList`) with O(1) neighbor lookups and degree computation
171
+ - Atom valence and hydrogen count: `atom_degree()`, `implicit_hydrogen_count()`, `total_hydrogen_count()` with default valence table for ~20 elements
172
+ - SSSR ring perception via spanning-tree + GF(2) linear independence: `sssr()`, `ring_sizes()`, `smallest_ring_size()`
173
+ - Hybridization inference from bond topology: `atom_hybridization()`, `all_hybridizations()` (S/SP/SP2/SP3/SP3D/SP3D2)
174
+ - Aromaticity perception: two-stage (trust file annotations + Hückel 4n+2 rule on SSSR rings) with support for pyrrole, furan, thiophene
175
+ - Conjugation detection: aromatic bonds, SP2-SP2 single bonds, unsaturated bond chains
176
+ - OGB-compatible GNN featurizer: `ogb_atom_features()` [N, 9], `ogb_bond_features()` [E, 3], `ogb_graph_features()` with directed edge index
177
+ - ECFP/Morgan fingerprints: `ecfp()` bit vector, `ecfp_counts()` hash map, Tanimoto similarity — first pure-Rust implementation
178
+ - Gasteiger-Marsili partial charges: PEOE iterative algorithm with ~15 atom type parameters
179
+ - CIP chirality perception: `atom_chirality()`, `all_chiralities()` with R/S determination from 3D signed volume
180
+ - Bond inference from 3D coordinates: `infer_bonds()`, `infer_bonds_with_config()` using covalent radii
181
+ - Cutoff-based neighbor list for 3D GNNs: `neighbor_list()` with directed edges and distances (geometry feature)
182
+ - Bond angle and dihedral angle computation: `bond_angle()`, `dihedral_angle()`, `all_bond_angles()`, `all_dihedral_angles()` (geometry feature)
183
+ - Python bindings for all new ML features with NumPy array outputs (`get_ogb_atom_features_array()`, `get_ecfp_array()`, `get_gasteiger_charges_array()`)
184
+ - Python examples: format conversion, batch analysis, geometry operations, ML features
185
+ - Rust example: `examples/ml_features.rs`
186
+
187
+ ## [0.5.0] - 2026-02-17
188
+
189
+ ### Added
190
+
191
+ - Transparent gzip decompression for all file parsers (optional `gzip` feature)
192
+ - XYZ format parser with streaming support, atomic number input, and case normalization
193
+ - XYZ auto-detection in `parse_auto_*()` and `iter_auto_file()`
194
+ - Python bindings for XYZ parsing and gzip support
195
+ - PDBbind 2024 large-scale SDF benchmark test (27,670 files, 100% success rate, ~14k files/sec)
196
+
197
+ ## [0.4.0] - 2026-01-30
198
+
199
+ ### Added
200
+
201
+ - Python bindings via PyO3 (`sdfrust-python/` crate) with full API coverage
202
+ - NumPy integration for coordinate and atomic number arrays
203
+ - Streaming iterators for Python (`iter_sdf_file`, `iter_mol2_file`)
204
+ - Workspace configuration for multi-crate build
205
+
206
+ ## [0.3.0] - 2026-01-30
207
+
208
+ ### Added
209
+
210
+ - Molecular descriptors module: molecular weight, exact mass, heavy atom count, bond type counts
211
+ - Topological descriptors: ring count, ring atoms/bonds detection, rotatable bond count
212
+ - Element data table (~30 elements with IUPAC 2021 atomic weights)
213
+ - Convenience descriptor methods on `Molecule`
214
+
215
+ ## [0.2.0] - 2025-01-29
216
+
217
+ ### Added
218
+
219
+ - SDF V3000 parser and writer with stereogroups, SGroups, and collections
220
+ - Automatic V2000/V3000 format detection and auto-format writing
221
+ - MOL2 (TRIPOS) parser with streaming support
222
+ - Performance benchmarks (~220k mol/s, 4-7x faster than RDKit)
223
+
224
+ ## [0.1.0] - 2025-01-25
225
+
226
+ ### Added
227
+
228
+ - SDF V2000 parser with atom/bond/property blocks and M CHG/M ISO lines
229
+ - SDF V2000 writer with property preservation and multi-molecule output
230
+ - Core types: `Molecule`, `Atom`, `Bond`, `BondOrder`, `BondStereo`
231
+ - Molecule methods: `formula()`, `centroid()`, `translate()`, `neighbors()`, `element_counts()`
232
+ - Memory-efficient streaming iterator for large multi-molecule files
233
+
234
+ [Unreleased]: https://github.com/HFooladi/sdfrust/compare/v0.7.0...HEAD
235
+ [0.7.0]: https://github.com/HFooladi/sdfrust/compare/v0.6.0...v0.7.0
236
+ [0.6.0]: https://github.com/HFooladi/sdfrust/compare/v0.5.0...v0.6.0
237
+ [0.5.0]: https://github.com/HFooladi/sdfrust/compare/v0.4.0...v0.5.0
238
+ [0.4.0]: https://github.com/HFooladi/sdfrust/compare/v0.3.0...v0.4.0
239
+ [0.3.0]: https://github.com/HFooladi/sdfrust/compare/v0.2.0...v0.3.0
240
+ [0.2.0]: https://github.com/HFooladi/sdfrust/compare/v0.1.0...v0.2.0
241
+ [0.1.0]: https://github.com/HFooladi/sdfrust/releases/tag/v0.1.0