bentopy 0.2.0a10__cp313-cp313-manylinux_2_34_x86_64.whl

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 (58) hide show
  1. bentopy-0.2.0a10.data/scripts/bentopy-init +0 -0
  2. bentopy-0.2.0a10.data/scripts/bentopy-pack +0 -0
  3. bentopy-0.2.0a10.data/scripts/bentopy-render +0 -0
  4. bentopy-0.2.0a10.data/scripts/bentopy-solvate +0 -0
  5. bentopy-0.2.0a10.dist-info/METADATA +358 -0
  6. bentopy-0.2.0a10.dist-info/RECORD +58 -0
  7. bentopy-0.2.0a10.dist-info/WHEEL +5 -0
  8. bentopy-0.2.0a10.dist-info/entry_points.txt +4 -0
  9. bentopy-0.2.0a10.dist-info/licenses/LICENSE.txt +13 -0
  10. bentopy-0.2.0a10.dist-info/top_level.txt +8 -0
  11. check/check.py +128 -0
  12. core/config/bent/lexer.rs +338 -0
  13. core/config/bent/parser.rs +1180 -0
  14. core/config/bent/writer.rs +205 -0
  15. core/config/bent.rs +149 -0
  16. core/config/compartment_combinations.rs +300 -0
  17. core/config/legacy.rs +768 -0
  18. core/config.rs +362 -0
  19. core/mod.rs +4 -0
  20. core/placement.rs +100 -0
  21. core/utilities.rs +1 -0
  22. core/version.rs +32 -0
  23. init/example.bent +74 -0
  24. init/main.rs +235 -0
  25. mask/config.py +153 -0
  26. mask/mask.py +308 -0
  27. mask/utilities.py +38 -0
  28. merge/merge.py +175 -0
  29. pack/args.rs +77 -0
  30. pack/main.rs +121 -0
  31. pack/mask.rs +940 -0
  32. pack/session.rs +176 -0
  33. pack/state/combinations.rs +31 -0
  34. pack/state/compartment.rs +44 -0
  35. pack/state/mask.rs +196 -0
  36. pack/state/pack.rs +187 -0
  37. pack/state/segment.rs +72 -0
  38. pack/state/space.rs +98 -0
  39. pack/state.rs +440 -0
  40. pack/structure.rs +185 -0
  41. pack/voxelize.rs +85 -0
  42. render/args.rs +109 -0
  43. render/limits.rs +73 -0
  44. render/main.rs +12 -0
  45. render/render.rs +393 -0
  46. render/structure.rs +264 -0
  47. solvate/args.rs +324 -0
  48. solvate/convert.rs +25 -0
  49. solvate/cookies.rs +185 -0
  50. solvate/main.rs +177 -0
  51. solvate/placement.rs +380 -0
  52. solvate/solvate.rs +244 -0
  53. solvate/structure.rs +160 -0
  54. solvate/substitute.rs +113 -0
  55. solvate/water/martini.rs +409 -0
  56. solvate/water/models.rs +150 -0
  57. solvate/water/tip3p.rs +658 -0
  58. solvate/water.rs +115 -0
core/config/legacy.rs ADDED
@@ -0,0 +1,768 @@
1
+ use std::{path::PathBuf, str::FromStr};
2
+
3
+ use serde::Deserialize;
4
+
5
+ pub use super::compartment_combinations::Expression as CombinationExpression;
6
+ use crate::core::config::{Axes, CompartmentID, Dimensions, defaults};
7
+
8
+ impl<'de> Deserialize<'de> for CombinationExpression {
9
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
10
+ where
11
+ D: serde::Deserializer<'de>,
12
+ {
13
+ let s = String::deserialize(deserializer)?;
14
+ CombinationExpression::from_str(&s).map_err(serde::de::Error::custom)
15
+ }
16
+ }
17
+
18
+ /// Avogadro's number (per mol).
19
+ const N_A: f64 = 6.0221415e23;
20
+
21
+ // TODO: I think it's cursed that we store these defaults here. I'd like to create a file
22
+ // collecting all of these consts, one day.
23
+ fn bead_radius_default() -> f32 {
24
+ defaults::BEAD_RADIUS as f32 // nm
25
+ }
26
+
27
+ fn max_tries_mult_default() -> u64 {
28
+ defaults::MAX_TRIES_MULT
29
+ }
30
+
31
+ fn max_tries_rot_div_default() -> u64 {
32
+ defaults::MAX_TRIES_ROT_DIV
33
+ }
34
+
35
+ #[derive(Deserialize)]
36
+ pub struct General {
37
+ pub seed: Option<u64>,
38
+ #[serde(default = "bead_radius_default")]
39
+ pub bead_radius: f32,
40
+ #[serde(default = "max_tries_mult_default")]
41
+ pub max_tries_mult: u64,
42
+ #[serde(default = "max_tries_rot_div_default")]
43
+ pub max_tries_rot_div: u64,
44
+ }
45
+
46
+ impl Default for General {
47
+ fn default() -> Self {
48
+ Self {
49
+ seed: Default::default(),
50
+ bead_radius: bead_radius_default(),
51
+ max_tries_mult: max_tries_mult_default(),
52
+ max_tries_rot_div: max_tries_rot_div_default(),
53
+ }
54
+ }
55
+ }
56
+
57
+ #[derive(Deserialize)]
58
+ #[serde(rename_all = "lowercase")]
59
+ pub enum Shape {
60
+ Spherical,
61
+ Cuboid,
62
+ None,
63
+ }
64
+
65
+ impl std::fmt::Display for Shape {
66
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67
+ match self {
68
+ Shape::Spherical => "spherical",
69
+ Shape::Cuboid => "cuboid",
70
+ Shape::None => "empty ('none')",
71
+ }
72
+ .fmt(f)
73
+ }
74
+ }
75
+
76
+ #[derive(Deserialize)]
77
+ #[serde(rename_all = "lowercase")]
78
+ pub enum Mask {
79
+ Shape(Shape),
80
+ Analytical {
81
+ shape: Shape,
82
+ center: Option<[f32; 3]>,
83
+ radius: Option<f32>,
84
+ },
85
+ Voxels {
86
+ path: PathBuf,
87
+ },
88
+ Combination(CombinationExpression),
89
+ }
90
+
91
+ #[derive(Deserialize)]
92
+ pub struct Compartment {
93
+ pub id: CompartmentID,
94
+ #[serde(flatten)]
95
+ pub mask: Mask,
96
+ }
97
+
98
+ impl Compartment {
99
+ pub fn is_predefined(&self) -> bool {
100
+ match &self.mask {
101
+ Mask::Shape(_) | Mask::Analytical { .. } | Mask::Voxels { .. } => true,
102
+ Mask::Combination(_) => false,
103
+ }
104
+ }
105
+ }
106
+
107
+ pub(crate) fn true_by_default() -> bool {
108
+ true
109
+ }
110
+
111
+ #[derive(Deserialize)]
112
+ pub struct Space {
113
+ pub size: Dimensions,
114
+ pub resolution: f32,
115
+ pub compartments: Vec<Compartment>,
116
+ #[serde(default = "true_by_default")]
117
+ pub periodic: bool,
118
+ // TODO: constraint system (satisfied _somewhat_ by the notion of a rule).
119
+ }
120
+
121
+ #[derive(Deserialize)]
122
+ #[serde(untagged)]
123
+ pub enum RuleExpression {
124
+ Rule(String),
125
+ Or(Vec<RuleExpression>),
126
+ }
127
+
128
+ fn parse_axes<'de, D>(deserializer: D) -> Result<Axes, D::Error>
129
+ where
130
+ D: serde::de::Deserializer<'de>,
131
+ {
132
+ let s = String::deserialize(deserializer)?;
133
+ s.parse().map_err(serde::de::Error::custom)
134
+ }
135
+
136
+ #[derive(Deserialize)]
137
+ pub struct Segment {
138
+ pub name: String,
139
+ pub tag: Option<String>,
140
+ #[serde(flatten)]
141
+ pub quantity: Quantity,
142
+ pub path: PathBuf,
143
+ pub compartments: Vec<CompartmentID>,
144
+ #[serde(default)]
145
+ pub rules: Vec<RuleExpression>,
146
+ #[serde(default, deserialize_with = "parse_axes")]
147
+ pub rotation_axes: Axes,
148
+ #[serde(default)]
149
+ pub initial_rotation: [f32; 3],
150
+ // TODO: center?
151
+ }
152
+
153
+ #[derive(Deserialize, Clone, Copy)]
154
+ #[serde(rename_all = "lowercase")]
155
+ pub enum Quantity {
156
+ Number(usize),
157
+ /// Concentration in mol/L.
158
+ Concentration(f64),
159
+ }
160
+
161
+ impl Quantity {
162
+ /// Determine the number of segments that is implied by this [`Quantity`].
163
+ ///
164
+ /// In case this `Quantity` is a [`Quantity::Concentration`], the number of segments is
165
+ /// lazily determined from the provided `volume`, and rounded.
166
+ ///
167
+ /// The value returned by `volume` must be in cubic nanometers (nm³).
168
+ pub fn bake<F: Fn() -> f64>(&self, volume: F) -> usize {
169
+ match *self {
170
+ Quantity::Number(n) => n,
171
+ Quantity::Concentration(c) => {
172
+ // n = N_A * c * V
173
+ let v = volume() * 1e-24; // From nm³ to L.
174
+ let n = N_A * c * v;
175
+ f64::round(n) as usize
176
+ }
177
+ }
178
+ }
179
+
180
+ /// Returns whether the contained value can be interpreted as resulting in zero placements.
181
+ ///
182
+ /// When the quantity is a `Number(0)` or `Concentration(0.0)`, the baked number is certainly
183
+ /// zero. When `Number(n)` for `n > 0`, the baked number is certainly not zero.
184
+ ///
185
+ /// But, in case of a positive concentration, whether the final number is zero or not depends
186
+ /// on the associated volume.
187
+ ///
188
+ /// If the concentration is smaller than zero, it is treated as a zero.
189
+ pub fn is_zero(&self) -> bool {
190
+ match *self {
191
+ Quantity::Number(n) => n == 0,
192
+ Quantity::Concentration(c) => c <= 0.0,
193
+ }
194
+ }
195
+ }
196
+
197
+ impl std::fmt::Display for Quantity {
198
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199
+ match self {
200
+ Quantity::Number(n) => write!(f, "{n} instances"),
201
+ Quantity::Concentration(c) => write!(f, "{c} mol/L"),
202
+ }
203
+ }
204
+ }
205
+
206
+ pub type TopolIncludes = Vec<String>;
207
+
208
+ #[derive(Deserialize)]
209
+ pub struct Output {
210
+ pub title: String,
211
+ pub topol_includes: Option<TopolIncludes>,
212
+ }
213
+
214
+ #[derive(Deserialize)]
215
+ pub struct Config {
216
+ #[serde(default)]
217
+ pub general: General,
218
+ pub space: Space,
219
+ pub segments: Vec<Segment>,
220
+ pub output: Output,
221
+ }
222
+
223
+ mod convert {
224
+ use std::collections::{HashMap, HashSet};
225
+
226
+ use super::*;
227
+ use crate::core::config;
228
+ use crate::core::config::legacy::convert::rule::parse_rule;
229
+
230
+ impl From<CombinationExpression> for config::Expr<CompartmentID> {
231
+ fn from(ce: CombinationExpression) -> Self {
232
+ type Expr = config::Expr<CompartmentID>;
233
+ fn unflatten(
234
+ expressions: Vec<CombinationExpression>,
235
+ binary: impl Fn(Box<Expr>, Box<Expr>) -> Expr,
236
+ ) -> Expr {
237
+ // We recursively convert the children first.
238
+ let exprs: Vec<config::Expr<CompartmentID>> = expressions
239
+ .into_iter()
240
+ .map(|expression| expression.into())
241
+ .collect();
242
+ // Then, we stitch them together into a tree.
243
+ let flat = exprs;
244
+ flat.into_iter()
245
+ .reduce(|acc, item| binary(Box::new(acc), Box::new(item)))
246
+ .expect("TODO")
247
+ }
248
+
249
+ type CE = CombinationExpression;
250
+ match ce {
251
+ CE::Id(id) => config::Expr::Term(id),
252
+ CE::Not(expression) => config::Expr::Not(Box::new((*expression).into())),
253
+ CE::Union(expressions) => unflatten(expressions, config::Expr::Or),
254
+ CE::Intersect(expressions) => unflatten(expressions, config::Expr::And),
255
+ }
256
+ }
257
+ }
258
+
259
+ impl From<Mask> for config::Mask {
260
+ fn from(mask: Mask) -> Self {
261
+ match mask {
262
+ Mask::Shape(shape) => match shape {
263
+ // TODO: This sucks but is true.
264
+ Shape::Spherical => panic!("a sphere without a radius is an undefined shape"),
265
+ Shape::Cuboid | Shape::None => config::Mask::All,
266
+ },
267
+ Mask::Analytical {
268
+ shape: Shape::Spherical,
269
+ center,
270
+ radius,
271
+ } => config::Mask::Shape(config::Shape::Sphere {
272
+ center: match center {
273
+ None => config::Anchor::Center,
274
+ Some(center) => config::Anchor::Point(center),
275
+ },
276
+ // TODO: This sucks but is true.
277
+ radius: radius.expect("a sphere without a radius is an undefined shape"),
278
+ }),
279
+ Mask::Analytical {
280
+ shape: Shape::Cuboid | Shape::None,
281
+ // Cursed that these could be set, but we'll just keep quit about it.
282
+ center: _,
283
+ radius: _,
284
+ } => config::Mask::All,
285
+ Mask::Voxels { path } => config::Mask::Voxels(path),
286
+ Mask::Combination(expression) => {
287
+ // TODO: This conversion sucks and will be changed.
288
+ config::Mask::Combination(expression.into())
289
+ }
290
+ }
291
+ }
292
+ }
293
+
294
+ impl From<Compartment> for config::Compartment {
295
+ fn from(compartment: Compartment) -> Self {
296
+ let Compartment { id, mask } = compartment;
297
+ config::Compartment {
298
+ id,
299
+ mask: mask.into(),
300
+ }
301
+ }
302
+ }
303
+
304
+ impl From<Segment> for config::Segment {
305
+ fn from(segment: Segment) -> Self {
306
+ let Segment {
307
+ name,
308
+ tag,
309
+ quantity,
310
+ path,
311
+ compartments,
312
+ rules,
313
+ rotation_axes,
314
+ initial_rotation,
315
+ } = segment;
316
+
317
+ // As you can judge from the comments in the upcoming section, some non-trivial things
318
+ // are happening here. For context, we need to do some juggling between the different
319
+ // data types. This is a complex affair that is coordinated over the other conversion
320
+ // functions as well. So, to understand this, make sure you also grasp how canonical
321
+ // ids are used in the root From<Config> section as well.
322
+
323
+ // We don't support this, anymore. It is still possible to rotate the contents of the
324
+ // structure file, of course.
325
+ if initial_rotation != <[f32; 3]>::default() {
326
+ unimplemented!("segment initial rotation is deprecated")
327
+ }
328
+
329
+ // This is a bit complicated. We need to
330
+ // (1) come up with a label for each unique RuleExpression in the file,
331
+ // (2) those rule expressions need to be formulated as a constraint and
332
+ // pushed to the constraints list. This is done in the conversion of
333
+ // legacy::Config to config::Config.
334
+ // Note that this is inefficient. That does not matter, since this just serves to
335
+ // convert a legacy format to the new format. It lets us write these conversions in
336
+ // such a way that the functions don't have to coordinate with each other which
337
+ // makes them nice and seperate. That is preferred here.
338
+ let compartment_ids_from_rules = rules
339
+ .iter()
340
+ .map(|re| {
341
+ let legacy_rule = parse_rule(re).expect("TODO");
342
+ rule::canonical_id_compartment(&legacy_rule)
343
+ })
344
+ // Only retain unique rules.
345
+ .collect::<HashSet<_>>();
346
+
347
+ // Same goes for the rotation_axes, if non-default. This notion is now conceived of as
348
+ // a rule, instead of as a per-segment property. We also come up with an injective id,
349
+ // here, and we assign it as the sole rule in the rules field.
350
+ let axes_rule = if rotation_axes != Default::default() {
351
+ let id = rule::canonical_id_rotation_axes(rotation_axes);
352
+ vec![id].into_boxed_slice()
353
+ } else {
354
+ Default::default()
355
+ };
356
+
357
+ // The list of compartments represents a union of masks that can be merged together to
358
+ // provide the accessible space for a segment. In the legacy format, where rules were
359
+ // applied per segment, the presence of rules meant that the union of compartments was
360
+ // intersected with the masks distilled from the rules. In other words, the rules were
361
+ // _applied_ to the compartments.
362
+ // Therefore, it would be incorrect to simply merge the compartment_ids_from_rules with
363
+ // the provided compartment ids, since that would create a union of the explicit
364
+ // compartment masks and the virtual masks distilled from the rules.
365
+ // Instead, if any legacy rules are present, we need to create a special compartment
366
+ // not only for the rule, but also for the union of compartment ids intersected with
367
+ // the new legacy rule compartment.
368
+ // From legacy::Segment { compartments: [a, b, c], rules: within 10 of d }, we go to
369
+ // config::Segment { compartment_ids: [{apply/{and/{or/a'b'c}'{win/d'10}}}] }.
370
+ // In From<Config>, this {apply/...} rule is actually added to the compartments
371
+ // section.
372
+ let compartment_ids = if compartment_ids_from_rules.is_empty() {
373
+ // No rules, so we can just take the union of compartments directly.
374
+ compartments.into_boxed_slice()
375
+ } else {
376
+ let rule_ids = compartment_ids_from_rules.into_iter().collect::<Vec<_>>();
377
+ let id = rule::canonical_id_apply(&compartments, &rule_ids);
378
+ vec![id].into_boxed_slice()
379
+ };
380
+
381
+ // Pfhew... that sucked. But we're here now.
382
+ config::Segment {
383
+ name,
384
+ tag,
385
+ quantity: quantity.into(),
386
+ path,
387
+ compartment_ids,
388
+ rules: axes_rule,
389
+ }
390
+ }
391
+ }
392
+
393
+ impl From<Quantity> for config::Quantity {
394
+ fn from(quantity: Quantity) -> Self {
395
+ match quantity {
396
+ Quantity::Number(n) => config::Quantity::Number(n as u64),
397
+ Quantity::Concentration(c) => config::Quantity::Concentration(c),
398
+ }
399
+ }
400
+ }
401
+
402
+ impl From<Config> for config::Config {
403
+ fn from(config: Config) -> Self {
404
+ let Config {
405
+ general:
406
+ General {
407
+ seed,
408
+ bead_radius,
409
+ max_tries_mult,
410
+ max_tries_rot_div,
411
+ },
412
+ space:
413
+ Space {
414
+ size,
415
+ resolution,
416
+ compartments,
417
+ periodic,
418
+ },
419
+ segments,
420
+ output:
421
+ Output {
422
+ title,
423
+ topol_includes,
424
+ },
425
+ } = config;
426
+
427
+ // More cursed canonical rule id logic.
428
+ // Together with the juggling in From<Segment>, the following constitutes a certified
429
+ // 'tricky bit'. We make the assumption here that we created id-constraint pairs in an
430
+ // injective manner. That is, one id has a single, unique rule associated with it, and
431
+ // vice versa. The management of the rule keys throughout this conversion code aims to
432
+ // uphold that.
433
+ // TODO: This can all be made less.. unclear if we just move it all together into one
434
+ // happy monolithic family. Consider.
435
+
436
+ // The legacy rules are now considered compartment declarations. We go through the
437
+ // segments and collect all of their rules into a set and for each of those items we
438
+ // come up with the canonical id the From<Segment> implementation also generated, along
439
+ // with the compartment mask definition that fits the bill.
440
+ // TODO: Ordering concerns from using HashMaps here?
441
+ let compartments = {
442
+ let mut legacy_rules = HashMap::new();
443
+ let mut rule_applications = HashMap::new();
444
+ for segment in &segments {
445
+ let mut segment_legacy_rules = HashMap::new();
446
+ for re in &segment.rules {
447
+ // First, we do a legacy parse of the rule.
448
+ let legacy_rule = parse_rule(re).expect("TODO");
449
+ // Come up with a unique id for this rule that will match how a segment
450
+ // converts the rule into the same id.
451
+ let id = rule::canonical_id_compartment(&legacy_rule);
452
+ segment_legacy_rules.insert(id, legacy_rule);
453
+ }
454
+
455
+ // Here comes another tricky bit.
456
+ if segment_legacy_rules.is_empty() {
457
+ // If there are no legacy rules, we can simply take the segment's list of
458
+ // compartments, like described in From<Segment>. That means that we don't
459
+ // have to do anything special here.
460
+ continue;
461
+ }
462
+
463
+ let segment_legacy_rule_ids =
464
+ segment_legacy_rules.keys().cloned().collect::<Vec<_>>();
465
+ let rule_application_id =
466
+ rule::canonical_id_apply(&segment.compartments, &segment_legacy_rule_ids);
467
+ let rule_application = (segment.compartments.clone(), segment_legacy_rule_ids);
468
+
469
+ legacy_rules.extend(segment_legacy_rules);
470
+ rule_applications.insert(rule_application_id, rule_application);
471
+ }
472
+
473
+ // Now we make actual compartments from the hashmaps.
474
+ let legacy_rules = legacy_rules.into_iter().map(|(id, legacy_rule)| {
475
+ // And convert the legacy Rule into a Mask.
476
+ let mask = legacy_rule.into_mask();
477
+ config::Compartment { id, mask }
478
+ });
479
+ let rule_applications = rule_applications.into_iter().map(|(id, application)| {
480
+ // And convert the legacy rule applications into a Mask.
481
+ use config::Expr;
482
+ let (compartment_ids, legacy_rule_ids) = application;
483
+ let compartments = compartment_ids
484
+ .into_iter()
485
+ .map(Expr::Term)
486
+ .reduce(|acc, r| Expr::Or(Box::new(acc), Box::new(r)))
487
+ .expect("a rules combination expression cannot be empty");
488
+ let rules = legacy_rule_ids
489
+ .into_iter()
490
+ .map(Expr::Term)
491
+ .reduce(|acc, r| Expr::And(Box::new(acc), Box::new(r)))
492
+ .expect("a rules combination expression cannot be empty");
493
+ let mask = config::Mask::Combination(Expr::And(
494
+ Box::new(compartments),
495
+ Box::new(rules),
496
+ ));
497
+ config::Compartment { id, mask }
498
+ });
499
+
500
+ compartments
501
+ .into_iter()
502
+ .map(Into::into)
503
+ .chain(legacy_rules)
504
+ .chain(rule_applications)
505
+ .collect()
506
+ };
507
+
508
+ // Currently, constraints can only be rotation axes declarations. Look through all the
509
+ // segments to see if there are non-default rotation axes set. For each of those,
510
+ // create a constraint with its canonical id that will also be generated by
511
+ // From<Segment> and the appropriate Rule. We collect them in a set to automatically
512
+ // retain only unique constraints.
513
+ let constraints = {
514
+ let mut constraints = HashSet::new();
515
+ for segment in &segments {
516
+ let rotation_axes = segment.rotation_axes;
517
+ if rotation_axes != Default::default() {
518
+ let id = rule::canonical_id_rotation_axes(rotation_axes);
519
+ let constraint = config::Constraint {
520
+ id,
521
+ rule: config::Rule::RotationAxes(rotation_axes),
522
+ };
523
+ constraints.insert(constraint);
524
+ }
525
+ }
526
+ constraints.into_iter().collect()
527
+ };
528
+
529
+ // Run through some defaults for the general and space sections.
530
+ let bead_radius = if bead_radius != defaults::BEAD_RADIUS as f32 {
531
+ Some(bead_radius as f64)
532
+ } else {
533
+ None
534
+ };
535
+ let max_tries_mult = if max_tries_mult != defaults::MAX_TRIES_MULT {
536
+ Some(max_tries_mult)
537
+ } else {
538
+ None
539
+ };
540
+ let max_tries_rot_div = if max_tries_rot_div != defaults::MAX_TRIES_ROT_DIV {
541
+ Some(max_tries_rot_div)
542
+ } else {
543
+ None
544
+ };
545
+ // Bit silly, but let's match this already ugly structure for clarity.
546
+ let periodic = if periodic != defaults::PERIODIC {
547
+ Some(periodic)
548
+ } else {
549
+ None
550
+ };
551
+
552
+ config::Config {
553
+ general: config::General {
554
+ title: if title.is_empty() { None } else { Some(title) },
555
+ seed,
556
+ bead_radius,
557
+ max_tries_mult,
558
+ max_tries_rot_div,
559
+ rearrange_method: None,
560
+ },
561
+ space: config::Space {
562
+ dimensions: Some(size),
563
+ resolution: Some(resolution as f64),
564
+ periodic,
565
+ },
566
+ includes: topol_includes
567
+ .unwrap_or_default()
568
+ .into_iter()
569
+ .map(Into::into)
570
+ .collect(),
571
+ constraints,
572
+ compartments,
573
+ segments: segments.into_iter().map(Into::into).collect(),
574
+ }
575
+ }
576
+ }
577
+
578
+ // We're vendoring the Rule stuff until that can all be refactored out.
579
+ mod rule {
580
+ use std::num::ParseFloatError;
581
+ use std::str::FromStr;
582
+
583
+ use crate::core::config::{self, Axis, Limit, Op};
584
+
585
+ use super::{CompartmentID, RuleExpression};
586
+
587
+ // TODO: This should be part of the config parsing.
588
+ pub fn parse_rule(expr: &RuleExpression) -> Result<Rule, ParseRuleError> {
589
+ match expr {
590
+ RuleExpression::Rule(s) => Rule::from_str(s),
591
+ RuleExpression::Or(exprs) => Ok(Rule::Or(
592
+ exprs.iter().map(parse_rule).collect::<Result<_, _>>()?,
593
+ )),
594
+ }
595
+ }
596
+
597
+ pub fn canonical_id_compartment(rule: &Rule) -> String {
598
+ match rule {
599
+ Rule::Position(Limit { axis, op, value }) => {
600
+ let op = match op {
601
+ Op::LessThan => "lt",
602
+ Op::GreaterThan => "gt",
603
+ };
604
+ format!("{{lim/{axis}'{op}'{value}}}")
605
+ }
606
+ Rule::IsCloser(id, distance) => {
607
+ format!("{{win/{id}'{distance}}}")
608
+ }
609
+ Rule::Or(rules) => {
610
+ let ids = rules
611
+ .iter()
612
+ .map(canonical_id_compartment)
613
+ .collect::<Vec<_>>()
614
+ .join("'");
615
+ format!("{{or/{ids}}}")
616
+ }
617
+ }
618
+ }
619
+
620
+ pub fn canonical_id_rotation_axes(axes: config::Axes) -> String {
621
+ let axes = axes
622
+ .list()
623
+ .iter()
624
+ .map(ToString::to_string)
625
+ .collect::<String>();
626
+ format!("{{axes/{axes}}}")
627
+ }
628
+
629
+ pub fn canonical_id_apply(
630
+ compartment_ids: &[CompartmentID],
631
+ rule_ids: &[CompartmentID],
632
+ ) -> String {
633
+ assert!(
634
+ !compartment_ids.is_empty(),
635
+ "expected at least one compartment"
636
+ );
637
+ assert!(!rule_ids.is_empty(), "expected at least one rule");
638
+
639
+ let rids = rule_ids.join("'");
640
+ let cids = compartment_ids.join("'");
641
+ format!("{{apply/{rids}/to/{cids}}}")
642
+ }
643
+
644
+ #[derive(Debug, Clone, PartialEq)]
645
+ pub enum Rule {
646
+ Position(Limit),
647
+ IsCloser(CompartmentID, f32),
648
+
649
+ /// A set of rules where any of them can be true for this [`Rule`] to apply.
650
+ Or(Vec<Rule>),
651
+ }
652
+
653
+ impl Rule {
654
+ pub(crate) fn into_mask(self) -> config::Mask {
655
+ use config::Expr;
656
+ match self {
657
+ Rule::Position(limit) => config::Mask::Limits(config::Expr::Term(limit)),
658
+ Rule::IsCloser(id, distance) => config::Mask::Within { distance, id },
659
+ Rule::Or(rules) => config::Mask::Combination(
660
+ rules
661
+ .into_iter()
662
+ .map(|rule| Expr::Term(canonical_id_compartment(&rule)))
663
+ .reduce(|acc, r| Expr::Or(Box::new(acc), Box::new(r)))
664
+ .expect("a rules combination expression cannot be empty"),
665
+ ),
666
+ }
667
+ }
668
+ }
669
+
670
+ impl FromStr for Rule {
671
+ type Err = ParseRuleError;
672
+
673
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
674
+ let trimmed = s.trim();
675
+ let mut words = trimmed.split_whitespace();
676
+ let keyword = words.next().ok_or(ParseRuleError::Empty)?;
677
+ match keyword {
678
+ kind @ ("less_than" | "greater_than") => {
679
+ let axis = words
680
+ .next()
681
+ .ok_or(ParseRuleError::SyntaxError("expected axis".to_string()))?
682
+ .parse()
683
+ .map_err(ParseRuleError::ParseAxisError)?;
684
+ let value = words
685
+ .next()
686
+ .ok_or(ParseRuleError::SyntaxError(
687
+ "expected scalar value".to_string(),
688
+ ))?
689
+ .parse()
690
+ .map_err(ParseRuleError::ParseScalarError)?;
691
+
692
+ let poscon = match kind {
693
+ "greater_than" => Limit {
694
+ axis,
695
+ op: Op::GreaterThan,
696
+ value,
697
+ },
698
+ "less_than" => Limit {
699
+ axis,
700
+ op: Op::LessThan,
701
+ value,
702
+ },
703
+ _ => unreachable!(), // By virtue of this branch's pattern.
704
+ };
705
+ Ok(Rule::Position(poscon))
706
+ }
707
+ "is_closer_to" => {
708
+ let compartment_id = words.next().ok_or(ParseRuleError::SyntaxError(
709
+ "expected compartment id".to_string(),
710
+ ))?;
711
+ let distance = words
712
+ .next()
713
+ .ok_or(ParseRuleError::SyntaxError(
714
+ "expected scalar value".to_string(),
715
+ ))?
716
+ .parse()
717
+ .map_err(ParseRuleError::ParseScalarError)?;
718
+
719
+ Ok(Rule::IsCloser(compartment_id.to_string(), distance))
720
+ }
721
+ unknown => Err(ParseRuleError::UnknownKeyword(unknown.to_string())),
722
+ }
723
+ }
724
+ }
725
+
726
+ #[derive(Debug, Clone)]
727
+ pub enum ParseRuleError {
728
+ Empty,
729
+ UnknownKeyword(String),
730
+ SyntaxError(String),
731
+ ParseScalarError(ParseFloatError),
732
+ ParseAxisError(String),
733
+ }
734
+
735
+ impl std::fmt::Display for ParseRuleError {
736
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
737
+ match self {
738
+ ParseRuleError::Empty => write!(f, "no rule keyword was provided"),
739
+ ParseRuleError::UnknownKeyword(unknown) => {
740
+ write!(f, "encountered an unknown keyword: {unknown:?}")
741
+ }
742
+ ParseRuleError::SyntaxError(err) => write!(f, "syntax error: {err}"),
743
+ ParseRuleError::ParseScalarError(err) => {
744
+ write!(f, "could not parse float: {err}")
745
+ }
746
+ ParseRuleError::ParseAxisError(err) => write!(f, "could not parse axis: {err}"),
747
+ }
748
+ }
749
+ }
750
+
751
+ impl std::error::Error for ParseRuleError {}
752
+
753
+ impl FromStr for Axis {
754
+ type Err = String;
755
+
756
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
757
+ match s {
758
+ "x" => Ok(Self::X),
759
+ "y" => Ok(Self::Y),
760
+ "z" => Ok(Self::Z),
761
+ weird => Err(format!(
762
+ "expected one of 'x', 'y', or 'z', but found {weird:?}"
763
+ )),
764
+ }
765
+ }
766
+ }
767
+ }
768
+ }