Skip to content

Commit da7aab4

Browse files
Peariformeclaude
andauthored
feat: implement molecular weight calculation (Mn, monoisotopic, ByTargetMn, ByExactMass) (#2)
- average_mass() and monoisotopic_mass() computed from the concrete SMILES via opensmiles node/atom API (heavy atoms + implicit H) - PolymerChain.mn populated automatically at build time - BuildStrategy::ByTargetMn and ByExactMass implemented via linear interpolation on MW(n=1) and MW(n=2) - 18 integration tests covering PE / PP / PS and all build strategies - Criterion benchmarks: average_mass, monoisotopic_mass, by_target_mn - Workspace deps switched from git to crates.io (bigsmiles/opensmiles "0.1") - benchmark.yml updated to run all bench files - README updated with new features and code examples Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e18189e commit da7aab4

File tree

10 files changed

+530
-57
lines changed

10 files changed

+530
-57
lines changed

.github/workflows/benchmark.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ jobs:
5959
6060
- name: Run benchmarks
6161
run: >
62-
cargo bench -p polysim-core --bench homopolymer
62+
cargo bench -p polysim-core
6363
-- --output-format bencher 2>&1 | tee benchmark_polysim.txt
6464
6565
- name: Store benchmark result

Cargo.lock

Lines changed: 6 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ repository = "https://github.com/Peariforme/polysim"
1414
homepage = "https://github.com/Peariforme/polysim"
1515

1616
[workspace.dependencies]
17-
# NOTE: switch to `bigsmiles = "0.1"` once the crate is available on crates.io.
18-
bigsmiles = { git = "https://github.com/Peariforme/bigsmiles-rs" }
19-
thiserror = "2"
20-
criterion = { version = "0.5", features = ["html_reports"] }
17+
bigsmiles = "0.1"
18+
opensmiles = "0.1"
19+
thiserror = "2"
20+
criterion = { version = "0.5", features = ["html_reports"] }

README.md

Lines changed: 74 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Polymer structure generator and physical property simulator written in Rust.
99

1010
Given a [BigSMILES](https://olsenlabmit.github.io/BigSMILES/docs/line_notation.html) string,
1111
polysim generates concrete polymer chains (as SMILES) and computes physical/chemical properties
12-
such as glass transition temperature, crystallisation tendency, molecular weight, and more.
12+
such as glass transition temperature, molecular weight, and more.
1313

1414
---
1515

@@ -21,11 +21,13 @@ such as glass transition temperature, crystallisation tendency, molecular weight
2121
| 🔜 | Random / alternating / block copolymers |
2222
| 🔜 | Branched polymers, graft copolymers, macromonomers |
2323
|| Chain length by repeat count |
24-
| 🔜 | Chain length by target Mn or exact mass |
24+
|| Chain length by target Mn (`ByTargetMn`) |
25+
|| Chain length by monoisotopic mass (`ByExactMass`) |
26+
|| Average molecular weight (IUPAC standard atomic weights) |
27+
|| Monoisotopic mass (most abundant isotope per element) |
2528
|| Tg estimation — Fox equation |
2629
| 🔜 | Tg estimation — Van Krevelen group contributions |
2730
| 🔜 | Crystallisation tendency |
28-
| 🔜 | Monoisotopic mass & average molecular weight |
2931
| 🔜 | Hildebrand solubility parameter |
3032
| 🔜 | Melting temperature Tm |
3133

@@ -39,51 +41,98 @@ such as glass transition temperature, crystallisation tendency, molecular weight
3941
polysim-core = "0.1"
4042
```
4143

44+
### Build a chain and read its molecular weight
45+
46+
```rust
47+
use polysim_core::{parse, builder::{linear::LinearBuilder, BuildStrategy}};
48+
49+
// Polyéthylène — 100 unités répétées
50+
let bs = parse("{[]CC[]}").unwrap();
51+
let chain = LinearBuilder::new(bs, BuildStrategy::ByRepeatCount(100))
52+
.homopolymer()
53+
.unwrap();
54+
55+
println!("Repeat units : {}", chain.repeat_count); // 100
56+
println!("Mn : {:.1} g/mol", chain.mn); // 1410.7 g/mol
57+
println!("SMILES : {}…", &chain.smiles[..20]);
58+
```
59+
60+
### Target a specific Mn
61+
4262
```rust
4363
use polysim_core::{parse, builder::{linear::LinearBuilder, BuildStrategy}};
4464

45-
// Generate a polystyrene chain with 50 repeat units
46-
let bs = parse("{[]CC(c1ccccc1)[]}").unwrap();
47-
let chain = LinearBuilder::new(bs, BuildStrategy::ByRepeatCount(50))
65+
// Polypropylène — viser Mn ≈ 10 000 g/mol
66+
let bs = parse("{[]CC(C)[]}").unwrap();
67+
let chain = LinearBuilder::new(bs, BuildStrategy::ByTargetMn(10_000.0))
68+
.homopolymer()
69+
.unwrap();
70+
71+
println!("Repeat units : {}", chain.repeat_count); // ≈ 237
72+
println!("Mn réel : {:.1} g/mol", chain.mn); // ≈ 9 996 g/mol
73+
```
74+
75+
### Compute masses independently
76+
77+
```rust
78+
use polysim_core::{parse, builder::{linear::LinearBuilder, BuildStrategy},
79+
properties::molecular_weight::{average_mass, monoisotopic_mass}};
80+
81+
let bs = parse("{[]CC(c1ccccc1)[]}").unwrap(); // polystyrène
82+
let chain = LinearBuilder::new(bs, BuildStrategy::ByRepeatCount(10))
4883
.homopolymer()
4984
.unwrap();
5085

51-
println!("{}", chain.smiles); // full SMILES string
52-
println!("{}", chain.repeat_count); // 50
86+
println!("Masse moyenne : {:.2} g/mol", average_mass(&chain));
87+
println!("Masse monoisotopique: {:.2} g/mol", monoisotopic_mass(&chain));
5388
```
5489

90+
### Glass transition temperature
91+
5592
```rust
5693
use polysim_core::properties::thermal::tg_fox;
5794

58-
// Tg of a 70/30 PS/PMMA blend (PS: 373 K, PMMA: 378 K)
95+
// Tg d'un mélange 70/30 PS/PMMA (PS : 373 K, PMMA : 378 K)
5996
let tg = tg_fox(&[(0.70, 373.0), (0.30, 378.0)]);
6097
println!("Tg ≈ {tg:.1} K");
6198
```
6299

63100
---
64101

65-
## Polymer architectures
66-
67-
| Architecture | Builder | Key parameters |
68-
|---|---|---|
69-
| Homopolymer | `LinearBuilder::homopolymer` | repeat count or target Mn |
70-
| Random copolymer | `LinearBuilder::random_copolymer` | weight fraction per monomer |
71-
| Alternating copolymer | `LinearBuilder::alternating_copolymer` | exactly 2 repeat units |
72-
| Block copolymer | `LinearBuilder::block_copolymer` | length of each block |
73-
| Comb / branched | `BranchedBuilder::comb_polymer` | branch frequency |
74-
| Graft copolymer | `BranchedBuilder::graft_copolymer` | graft fraction |
75-
| Macromonomer | `BranchedBuilder::macromonomer` | side chain + end group |
76-
77-
### Build strategies
102+
## Build strategies
78103

79104
```rust
80105
pub enum BuildStrategy {
81-
ByRepeatCount(usize), // exact number of repeat units
82-
ByTargetMn(f64), // target number-average Mn in g/mol (🔜)
83-
ByExactMass(f64), // target monoisotopic mass in g/mol (🔜)
106+
/// Nombre exact d'unités répétées.
107+
ByRepeatCount(usize),
108+
109+
/// Mn cible (masse moléculaire moyenne, g/mol).
110+
/// Le nombre de répétitions est déduit par extrapolation linéaire.
111+
ByTargetMn(f64),
112+
113+
/// Masse monoisotopique cible (g/mol).
114+
/// Même logique que ByTargetMn mais avec les masses monoisotopiques.
115+
ByExactMass(f64),
84116
}
85117
```
86118

119+
Après construction, `chain.mn` contient toujours la masse moléculaire moyenne calculée,
120+
quelle que soit la stratégie utilisée.
121+
122+
---
123+
124+
## Polymer architectures
125+
126+
| Architecture | Builder | Statut |
127+
|---|---|---|
128+
| Homopolymer | `LinearBuilder::homopolymer` ||
129+
| Random copolymer | `LinearBuilder::random_copolymer` | 🔜 |
130+
| Alternating copolymer | `LinearBuilder::alternating_copolymer` | 🔜 |
131+
| Block copolymer | `LinearBuilder::block_copolymer` | 🔜 |
132+
| Comb / branched | `BranchedBuilder::comb_polymer` | 🔜 |
133+
| Graft copolymer | `BranchedBuilder::graft_copolymer` | 🔜 |
134+
| Macromonomer | `BranchedBuilder::macromonomer` | 🔜 |
135+
87136
---
88137

89138
## Workspace layout
@@ -97,8 +146,6 @@ polysim/
97146
│ │ ├── polymer/ # PolymerChain type
98147
│ │ └── properties/ # Tg, MW, …
99148
│ └── polysim-cli/ # command-line tool (not yet published)
100-
└── tests/
101-
└── integration.rs
102149
```
103150

104151
---

crates/polysim-core/Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ repository.workspace = true
1313
homepage.workspace = true
1414

1515
[dependencies]
16-
bigsmiles = { workspace = true }
17-
thiserror = { workspace = true }
16+
bigsmiles = { workspace = true }
17+
opensmiles = { workspace = true }
18+
thiserror = { workspace = true }
1819

1920
[dev-dependencies]
2021
bigsmiles = { workspace = true }
@@ -23,3 +24,7 @@ criterion = { workspace = true }
2324
[[bench]]
2425
name = "homopolymer"
2526
harness = false
27+
28+
[[bench]]
29+
name = "molecular_weight"
30+
harness = false
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use bigsmiles::parse;
2+
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
3+
use polysim_core::{
4+
builder::{linear::LinearBuilder, BuildStrategy},
5+
properties::molecular_weight::{average_mass, monoisotopic_mass},
6+
};
7+
8+
fn bench_average_mass(c: &mut Criterion) {
9+
let mut group = c.benchmark_group("molecular_weight/average_mass");
10+
11+
for n in [10usize, 100, 1_000] {
12+
// Pré-construire la chaîne — on benche uniquement le calcul de masse
13+
let bs = parse("{[]CC[]}").unwrap();
14+
let chain = LinearBuilder::new(bs, BuildStrategy::ByRepeatCount(n))
15+
.homopolymer()
16+
.unwrap();
17+
// Throughput = atomes lourds dans la chaîne (2n carbons)
18+
group.throughput(Throughput::Elements(2 * n as u64));
19+
group.bench_with_input(BenchmarkId::new("polyethylene", n), &chain, |b, chain| {
20+
b.iter(|| average_mass(chain));
21+
});
22+
}
23+
24+
// Polystyrène : atomes aromatiques, plus complexe à parser
25+
for n in [10usize, 100] {
26+
let bs = parse("{[]CC(c1ccccc1)[]}").unwrap();
27+
let chain = LinearBuilder::new(bs, BuildStrategy::ByRepeatCount(n))
28+
.homopolymer()
29+
.unwrap();
30+
// 8 atomes lourds par unité (2C aliphatique + 6C aromatique)
31+
group.throughput(Throughput::Elements(8 * n as u64));
32+
group.bench_with_input(BenchmarkId::new("polystyrene", n), &chain, |b, chain| {
33+
b.iter(|| average_mass(chain));
34+
});
35+
}
36+
37+
group.finish();
38+
}
39+
40+
fn bench_monoisotopic_mass(c: &mut Criterion) {
41+
let mut group = c.benchmark_group("molecular_weight/monoisotopic_mass");
42+
43+
for n in [10usize, 100, 1_000] {
44+
let bs = parse("{[]CC[]}").unwrap();
45+
let chain = LinearBuilder::new(bs, BuildStrategy::ByRepeatCount(n))
46+
.homopolymer()
47+
.unwrap();
48+
group.throughput(Throughput::Elements(2 * n as u64));
49+
group.bench_with_input(BenchmarkId::new("polyethylene", n), &chain, |b, chain| {
50+
b.iter(|| monoisotopic_mass(chain));
51+
});
52+
}
53+
54+
group.finish();
55+
}
56+
57+
fn bench_by_target_mn(c: &mut Criterion) {
58+
let mut group = c.benchmark_group("molecular_weight/by_target_mn");
59+
60+
// Bench complet : résolution de n + construction de la chaîne + calcul MW
61+
for target in [282.554f64, 2825.54, 28255.4] {
62+
let bs = parse("{[]CC[]}").unwrap();
63+
group.bench_with_input(
64+
BenchmarkId::new("polyethylene", target as usize),
65+
&(bs, target),
66+
|b, (bs, target)| {
67+
b.iter(|| {
68+
let bs2 = parse("{[]CC[]}").unwrap();
69+
let _ = bs2;
70+
LinearBuilder::new(bs.clone(), BuildStrategy::ByTargetMn(*target))
71+
.homopolymer()
72+
.unwrap()
73+
});
74+
},
75+
);
76+
}
77+
78+
group.finish();
79+
}
80+
81+
criterion_group!(
82+
benches,
83+
bench_average_mass,
84+
bench_monoisotopic_mass,
85+
bench_by_target_mn
86+
);
87+
criterion_main!(benches);

0 commit comments

Comments
 (0)