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.
- bentopy-0.2.0a10.data/scripts/bentopy-init +0 -0
- bentopy-0.2.0a10.data/scripts/bentopy-pack +0 -0
- bentopy-0.2.0a10.data/scripts/bentopy-render +0 -0
- bentopy-0.2.0a10.data/scripts/bentopy-solvate +0 -0
- bentopy-0.2.0a10.dist-info/METADATA +358 -0
- bentopy-0.2.0a10.dist-info/RECORD +58 -0
- bentopy-0.2.0a10.dist-info/WHEEL +5 -0
- bentopy-0.2.0a10.dist-info/entry_points.txt +4 -0
- bentopy-0.2.0a10.dist-info/licenses/LICENSE.txt +13 -0
- bentopy-0.2.0a10.dist-info/top_level.txt +8 -0
- check/check.py +128 -0
- core/config/bent/lexer.rs +338 -0
- core/config/bent/parser.rs +1180 -0
- core/config/bent/writer.rs +205 -0
- core/config/bent.rs +149 -0
- core/config/compartment_combinations.rs +300 -0
- core/config/legacy.rs +768 -0
- core/config.rs +362 -0
- core/mod.rs +4 -0
- core/placement.rs +100 -0
- core/utilities.rs +1 -0
- core/version.rs +32 -0
- init/example.bent +74 -0
- init/main.rs +235 -0
- mask/config.py +153 -0
- mask/mask.py +308 -0
- mask/utilities.py +38 -0
- merge/merge.py +175 -0
- pack/args.rs +77 -0
- pack/main.rs +121 -0
- pack/mask.rs +940 -0
- pack/session.rs +176 -0
- pack/state/combinations.rs +31 -0
- pack/state/compartment.rs +44 -0
- pack/state/mask.rs +196 -0
- pack/state/pack.rs +187 -0
- pack/state/segment.rs +72 -0
- pack/state/space.rs +98 -0
- pack/state.rs +440 -0
- pack/structure.rs +185 -0
- pack/voxelize.rs +85 -0
- render/args.rs +109 -0
- render/limits.rs +73 -0
- render/main.rs +12 -0
- render/render.rs +393 -0
- render/structure.rs +264 -0
- solvate/args.rs +324 -0
- solvate/convert.rs +25 -0
- solvate/cookies.rs +185 -0
- solvate/main.rs +177 -0
- solvate/placement.rs +380 -0
- solvate/solvate.rs +244 -0
- solvate/structure.rs +160 -0
- solvate/substitute.rs +113 -0
- solvate/water/martini.rs +409 -0
- solvate/water/models.rs +150 -0
- solvate/water/tip3p.rs +658 -0
- 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
|
+
}
|