Skip to content

Commit 3980055

Browse files
Peariformeclaude
andauthored
feat(phase1): implement all Phase 1 polymer architectures (#90)
* feat(core): implement Phase 1 polymer architectures (US-1.1.1 → US-1.4.2) ## Nouveaux builders ### LinearBuilder (linear.rs) - US-1.1.1: `random_copolymer(&[f64])` — copolymère statistique avec seed - US-1.1.2: `alternating_copolymer()` — séquence A-B-A-B cyclique - US-1.1.3: `block_copolymer(&[usize])` — diblock/triblock/multiblock - US-1.1.4: `gradient_copolymer(&GradientProfile)` — gradient linéaire ou sigmoïde - US-1.2.5: `cyclic_homopolymer()` — ring closure premier↔dernier atome - US-1.3.3: `with_end_groups()` — préfixe/suffixe BigSMILES inclus dans SMILES + Mn ### BranchedBuilder (branched.rs) - US-1.2.1: `comb_polymer(branch_every)` — peigne régulier backbone+branch - US-1.2.2: `graft_copolymer(graft_fraction, seed)` — greffage aléatoire reproductible - US-1.2.3: `star_polymer(arms)` — étoile 3–12 bras - US-1.2.4: `dendrimer(generation, branching_factor)` — dendrimère G1–G6 ### EnsembleBuilder (ensemble.rs) - `random_copolymer_ensemble`, `alternating_copolymer_ensemble`, `block_copolymer_ensemble`, `gradient_copolymer_ensemble` ## PolymerChain enrichi (US-1.3.1, US-1.3.2) - Nouveau champ `composition: Vec<MonomerUnit>` (fraction molaire par unité) - Nouveau champ `architecture: Architecture` (Linear/Star/Comb/Dendrimer/Cyclic/Gradient/Graft) - API builder fluent: `.with_composition()` et `.with_architecture()` ## CLI (US-1.4.1, US-1.4.2) - `polysim analyze` : `--arch random|alternating|block|gradient` - `polysim generate` : idem + `--gradient-profile linear|sigmoid`, `--gradient-f-start`, `--gradient-f-end` ## Tests: 35 nouveaux tests (tous verts) - `tests/copolymer.rs` : 17 tests random/alternating/block + ensembles - `tests/branched.rs` : 9 tests comb/graft/star/dendrimer - `tests/gradient_cyclic.rs` : 9 tests gradient/cyclic/end-groups Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add roadmap * test(core): add cyclic regression test for two-letter atoms (Cl) Vérifie que make_cyclic_smiles ne corrompt pas les atomes bi-lettres comme Cl lors de l'insertion du ring closure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2852e6e commit 3980055

File tree

17 files changed

+2971
-61
lines changed

17 files changed

+2971
-61
lines changed

ROADMAP.md

Lines changed: 945 additions & 0 deletions
Large diffs are not rendered by default.

crates/polysim-cli/src/commands/analyze.rs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,45 @@ use polysim_core::{
1111

1212
use crate::display;
1313
use crate::report::AnalysisResult;
14-
use crate::StrategyArgs;
14+
use crate::{Architecture, ArchitectureArgs, StrategyArgs};
1515

1616
/// Entry point for the `analyze` subcommand.
17-
pub fn run(bigsmiles_str: &str, args: &StrategyArgs) -> Result<(), i32> {
17+
pub fn run(
18+
bigsmiles_str: &str,
19+
args: &StrategyArgs,
20+
arch_args: &ArchitectureArgs,
21+
) -> Result<(), i32> {
1822
let bigsmiles = parse(bigsmiles_str).map_err(report_err)?;
1923

20-
let chain = LinearBuilder::new(bigsmiles.clone(), args.build_strategy())
21-
.homopolymer()
22-
.map_err(report_err)?;
24+
let mut builder = LinearBuilder::new(bigsmiles.clone(), args.build_strategy());
25+
if let Some(seed) = arch_args.copolymer_seed {
26+
builder = builder.seed(seed);
27+
}
28+
29+
let chain = match arch_args.arch {
30+
Architecture::Homo => builder.homopolymer(),
31+
Architecture::Random => {
32+
let fractions = arch_args.fractions.as_deref().unwrap_or(&[]);
33+
builder.random_copolymer(fractions)
34+
}
35+
Architecture::Alternating => builder.alternating_copolymer(),
36+
Architecture::Block => {
37+
let lengths = arch_args.block_lengths.as_deref().unwrap_or(&[]);
38+
builder.block_copolymer(lengths)
39+
}
40+
Architecture::Gradient => {
41+
let profile = arch_args.gradient_profile();
42+
builder.gradient_copolymer(&profile)
43+
}
44+
}
45+
.map_err(report_err)?;
2346

2447
let mono_mass = monoisotopic_mass(&chain);
2548

2649
let result = AnalysisResult {
2750
bigsmiles_str: bigsmiles_str.to_owned(),
2851
strategy_label: args.label(),
52+
architecture_label: arch_args.arch.label().to_owned(),
2953
begin_block: segments_to_smiles(bigsmiles.prefix_segments()),
3054
end_block: segments_to_smiles(bigsmiles.suffix_segments()),
3155
smiles: chain.smiles.clone(),

crates/polysim-cli/src/commands/generate.rs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use polysim_core::{
99
};
1010

1111
use crate::display;
12-
use crate::DistributionKind;
12+
use crate::{Architecture, ArchitectureArgs, DistributionKind};
1313

1414
/// Entry point for the `generate` subcommand.
1515
pub fn run(
@@ -19,6 +19,7 @@ pub fn run(
1919
distribution: &DistributionKind,
2020
num_chains: usize,
2121
seed: Option<u64>,
22+
arch_args: &ArchitectureArgs,
2223
) -> Result<(), i32> {
2324
let bs = parse(bigsmiles_str).map_err(report_err)?;
2425

@@ -36,11 +37,18 @@ pub fn run(
3637
eprintln!();
3738
}
3839

39-
let ensemble =
40-
build_ensemble(distribution, bs, mn, pdi, num_chains, seed).map_err(report_err)?;
40+
let ensemble = build_ensemble(distribution, bs, mn, pdi, num_chains, seed, arch_args)
41+
.map_err(report_err)?;
4142

4243
let stats = EnsembleStats::from_ensemble(&ensemble);
43-
display::print_ensemble_report(bigsmiles_str, distribution, mn, pdi, &stats);
44+
display::print_ensemble_report(
45+
bigsmiles_str,
46+
distribution,
47+
&arch_args.arch,
48+
mn,
49+
pdi,
50+
&stats,
51+
);
4452
Ok(())
4553
}
4654

@@ -51,11 +59,12 @@ fn build_ensemble(
5159
pdi: f64,
5260
num_chains: usize,
5361
seed: Option<u64>,
62+
arch_args: &ArchitectureArgs,
5463
) -> Result<PolymerEnsemble, PolySimError> {
5564
match distribution {
56-
DistributionKind::Flory => build(Flory, bs, mn, pdi, num_chains, seed),
57-
DistributionKind::LogNormal => build(LogNormal, bs, mn, pdi, num_chains, seed),
58-
DistributionKind::SchulzZimm => build(SchulzZimm, bs, mn, pdi, num_chains, seed),
65+
DistributionKind::Flory => build(Flory, bs, mn, pdi, num_chains, seed, arch_args),
66+
DistributionKind::LogNormal => build(LogNormal, bs, mn, pdi, num_chains, seed, arch_args),
67+
DistributionKind::SchulzZimm => build(SchulzZimm, bs, mn, pdi, num_chains, seed, arch_args),
5968
}
6069
}
6170

@@ -66,12 +75,29 @@ fn build<D: ChainLengthDistribution>(
6675
pdi: f64,
6776
num_chains: usize,
6877
seed: Option<u64>,
78+
arch_args: &ArchitectureArgs,
6979
) -> Result<PolymerEnsemble, PolySimError> {
7080
let mut builder = EnsembleBuilder::new(bs, dist, mn, pdi).num_chains(num_chains);
7181
if let Some(s) = seed {
7282
builder = builder.seed(s);
7383
}
74-
builder.homopolymer_ensemble()
84+
85+
match arch_args.arch {
86+
Architecture::Homo => builder.homopolymer_ensemble(),
87+
Architecture::Random => {
88+
let fractions = arch_args.fractions.as_deref().unwrap_or(&[]);
89+
builder.random_copolymer_ensemble(fractions)
90+
}
91+
Architecture::Alternating => builder.alternating_copolymer_ensemble(),
92+
Architecture::Block => {
93+
let ratios = arch_args.block_ratios.as_deref().unwrap_or(&[]);
94+
builder.block_copolymer_ensemble(ratios)
95+
}
96+
Architecture::Gradient => {
97+
let profile = arch_args.gradient_profile();
98+
builder.gradient_copolymer_ensemble(&profile)
99+
}
100+
}
75101
}
76102

77103
fn report_err(e: impl std::fmt::Display) -> i32 {

crates/polysim-cli/src/display.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ fn print_banner() {
2626

2727
fn print_summary(r: &AnalysisResult) {
2828
println!(" {:<11}{}", "BigSMILES".bold(), r.bigsmiles_str.yellow());
29+
println!(" {:<11}{}", "Arch".bold(), r.architecture_label.cyan());
2930
println!(" {:<11}{}", "Strategy".bold(), r.strategy_label);
3031
if let Some(ref bb) = r.begin_block {
3132
println!(" {:<11}{}", "Begin".bold(), bb.yellow());
@@ -131,19 +132,27 @@ fn add_mono_rows(table: &mut Table, r: &AnalysisResult) {
131132

132133
// ═══ Ensemble report ═════════════════════════════════════════════════════════
133134

134-
use crate::DistributionKind;
135+
use crate::{Architecture, DistributionKind};
135136
use polysim_core::properties::ensemble::EnsembleStats;
136137

137138
/// Prints the ensemble generation report to stdout.
138139
pub fn print_ensemble_report(
139140
bigsmiles_str: &str,
140141
distribution: &DistributionKind,
142+
architecture: &Architecture,
141143
target_mn: f64,
142144
target_pdi: f64,
143145
stats: &EnsembleStats,
144146
) {
145147
print_ensemble_banner();
146-
print_ensemble_summary(bigsmiles_str, distribution, target_mn, target_pdi, stats);
148+
print_ensemble_summary(
149+
bigsmiles_str,
150+
distribution,
151+
architecture,
152+
target_mn,
153+
target_pdi,
154+
stats,
155+
);
147156
print_ensemble_table(stats, target_mn, target_pdi);
148157
println!();
149158
}
@@ -161,11 +170,13 @@ fn print_ensemble_banner() {
161170
fn print_ensemble_summary(
162171
bigsmiles_str: &str,
163172
distribution: &DistributionKind,
173+
architecture: &Architecture,
164174
target_mn: f64,
165175
target_pdi: f64,
166176
stats: &EnsembleStats,
167177
) {
168178
println!(" {:<15}{}", "BigSMILES".bold(), bigsmiles_str.yellow());
179+
println!(" {:<15}{}", "Arch".bold(), architecture.label().cyan());
169180
println!(
170181
" {:<15}{}",
171182
"Distribution".bold(),

crates/polysim-cli/src/main.rs

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ enum Commands {
2828
/// Mn, Mw, dispersity, molecular formula, monoisotopic mass, and atom count.
2929
Analyze {
3030
/// BigSMILES string, e.g. "{[]CC[]}" for polyethylene.
31-
///
32-
/// The stochastic object {…} must contain exactly one repeat unit
33-
/// (homopolymer). Copolymers will be supported in a future release.
3431
bigsmiles: String,
3532

3633
#[command(flatten)]
3734
strategy: StrategyArgs,
35+
36+
#[command(flatten)]
37+
arch: ArchitectureArgs,
3838
},
3939

4040
/// Generate a polydisperse ensemble of polymer chains.
@@ -64,6 +64,9 @@ enum Commands {
6464
/// Random seed for reproducible results.
6565
#[arg(long)]
6666
seed: Option<u64>,
67+
68+
#[command(flatten)]
69+
arch: ArchitectureArgs,
6770
},
6871
}
6972

@@ -108,6 +111,94 @@ impl StrategyArgs {
108111
}
109112
}
110113

114+
/// Polymer architecture and copolymer parameters.
115+
#[derive(Args)]
116+
pub(crate) struct ArchitectureArgs {
117+
/// Polymer architecture.
118+
#[arg(
119+
long,
120+
value_enum,
121+
default_value = "homo",
122+
help_heading = "Architecture"
123+
)]
124+
pub(crate) arch: Architecture,
125+
126+
/// Weight fractions for random copolymer (comma-separated, e.g. "0.6,0.4").
127+
#[arg(long, value_delimiter = ',', help_heading = "Architecture")]
128+
pub(crate) fractions: Option<Vec<f64>>,
129+
130+
/// Block lengths for block copolymer analysis (comma-separated, e.g. "50,30").
131+
#[arg(long, value_delimiter = ',', help_heading = "Architecture")]
132+
pub(crate) block_lengths: Option<Vec<usize>>,
133+
134+
/// Block ratios for block copolymer ensemble (comma-separated, e.g. "0.5,0.5").
135+
#[arg(long, value_delimiter = ',', help_heading = "Architecture")]
136+
pub(crate) block_ratios: Option<Vec<f64>>,
137+
138+
/// Random seed for reproducible random/gradient copolymers.
139+
#[arg(long, help_heading = "Architecture")]
140+
pub(crate) copolymer_seed: Option<u64>,
141+
142+
/// Gradient profile shape (for --arch gradient).
143+
#[arg(
144+
long,
145+
value_enum,
146+
default_value = "linear",
147+
help_heading = "Architecture"
148+
)]
149+
pub(crate) gradient_profile: GradientProfileKind,
150+
151+
/// Starting fraction of monomer A (for --arch gradient).
152+
#[arg(long, default_value = "1.0", help_heading = "Architecture")]
153+
pub(crate) gradient_f_start: f64,
154+
155+
/// Ending fraction of monomer A (for --arch gradient).
156+
#[arg(long, default_value = "0.0", help_heading = "Architecture")]
157+
pub(crate) gradient_f_end: f64,
158+
}
159+
160+
impl ArchitectureArgs {
161+
pub(crate) fn gradient_profile(&self) -> polysim_core::GradientProfile {
162+
match self.gradient_profile {
163+
GradientProfileKind::Linear => polysim_core::GradientProfile::Linear {
164+
f_start: self.gradient_f_start,
165+
f_end: self.gradient_f_end,
166+
},
167+
GradientProfileKind::Sigmoid => polysim_core::GradientProfile::Sigmoid {
168+
f_start: self.gradient_f_start,
169+
f_end: self.gradient_f_end,
170+
},
171+
}
172+
}
173+
}
174+
175+
#[derive(Clone, ValueEnum)]
176+
pub(crate) enum GradientProfileKind {
177+
Linear,
178+
Sigmoid,
179+
}
180+
181+
#[derive(Clone, ValueEnum)]
182+
pub(crate) enum Architecture {
183+
Homo,
184+
Random,
185+
Alternating,
186+
Block,
187+
Gradient,
188+
}
189+
190+
impl Architecture {
191+
pub(crate) fn label(&self) -> &'static str {
192+
match self {
193+
Self::Homo => "Homopolymer",
194+
Self::Random => "Random copolymer",
195+
Self::Alternating => "Alternating copolymer",
196+
Self::Block => "Block copolymer",
197+
Self::Gradient => "Gradient copolymer",
198+
}
199+
}
200+
}
201+
111202
#[derive(Clone, ValueEnum)]
112203
pub(crate) enum DistributionKind {
113204
Flory,
@@ -131,8 +222,9 @@ fn main() {
131222
Commands::Analyze {
132223
bigsmiles,
133224
strategy,
225+
arch,
134226
} => {
135-
if let Err(code) = commands::analyze::run(&bigsmiles, &strategy) {
227+
if let Err(code) = commands::analyze::run(&bigsmiles, &strategy, &arch) {
136228
std::process::exit(code);
137229
}
138230
}
@@ -143,9 +235,10 @@ fn main() {
143235
distribution,
144236
num_chains,
145237
seed,
238+
arch,
146239
} => {
147240
if let Err(code) =
148-
commands::generate::run(&bigsmiles, mn, pdi, &distribution, num_chains, seed)
241+
commands::generate::run(&bigsmiles, mn, pdi, &distribution, num_chains, seed, &arch)
149242
{
150243
std::process::exit(code);
151244
}

crates/polysim-cli/src/report.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pub struct AnalysisResult {
33
pub bigsmiles_str: String,
44
pub strategy_label: String,
5+
pub architecture_label: String,
56
pub begin_block: Option<String>,
67
pub end_block: Option<String>,
78
pub smiles: String,

0 commit comments

Comments
 (0)