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
pack/state/segment.rs ADDED
@@ -0,0 +1,72 @@
1
+ use std::path::PathBuf;
2
+
3
+ use bentopy::core::config::{Axes, CompartmentID, Quantity};
4
+ use glam::{Mat3, Quat};
5
+
6
+ use crate::state::{ORDER, Rotation, Voxels};
7
+ use crate::structure::Structure;
8
+ use crate::voxelize::voxelize;
9
+
10
+ pub struct Segment {
11
+ pub name: String,
12
+ pub tag: Option<String>,
13
+ pub quantity: Quantity,
14
+ pub compartments: Box<[CompartmentID]>,
15
+ pub path: PathBuf,
16
+ pub rotation_axes: Axes,
17
+ pub(crate) structure: Structure,
18
+ /// The initial rotation of the structure must be applied before the random rotation.
19
+ pub(crate) initial_rotation: Rotation,
20
+ /// Invariant: This rotation must satisfy the constraints set by the `rotation_axes` field by
21
+ /// construction.
22
+ pub(crate) rotation: Rotation,
23
+ pub(crate) voxels: Option<Voxels>,
24
+ }
25
+
26
+ impl Segment {
27
+ /// Set a new rotation the [`Segment`].
28
+ ///
29
+ /// This invalidates the voxelization.
30
+ ///
31
+ /// The internal `rotation_axes` are taken into account when storing the rotation, such that a
32
+ /// rotation stored in a `Segment` is always internally consistent.
33
+ pub fn set_rotation(&mut self, rotation: Rotation) {
34
+ // FIXME: Assert it's a well-formed rotation?
35
+ // TODO: This seems slightly hacky, since we are converting the rotation between different
36
+ // formats a couple of times. It should be fine---we just lose an unimportant bit of
37
+ // accuracy on a random rotation---but perhaps it is more wise to store the rotation
38
+ // internally as a quaternion and only convert it to Mat3 when writing to the placement
39
+ // list.
40
+ let axes = &self.rotation_axes;
41
+ let (ax, ay, az) = Quat::from_mat3(&rotation).to_euler(ORDER);
42
+ let (ax, ay, az) = (
43
+ if axes.x { ax } else { 0.0 },
44
+ if axes.y { ay } else { 0.0 },
45
+ if axes.z { az } else { 0.0 },
46
+ );
47
+ self.rotation = Mat3::from_euler(ORDER, ax, ay, az);
48
+ self.voxels = None;
49
+ }
50
+
51
+ /// Get the correctly formed rotation of this [`Segment`].
52
+ pub fn rotation(&self) -> Rotation {
53
+ self.rotation * self.initial_rotation
54
+ }
55
+
56
+ /// Voxelize this [`Segment`] according to its current rotation.
57
+ ///
58
+ /// The voxelization can be accessed through [`Segment::voxels`].
59
+ pub fn voxelize(&mut self, resolution: f32, radius: f32) {
60
+ self.voxels = Some(voxelize(
61
+ &self.structure,
62
+ self.rotation(),
63
+ resolution,
64
+ radius,
65
+ ));
66
+ }
67
+
68
+ /// If available, return a reference to the voxels that represent this [`Segment`].
69
+ pub fn voxels(&self) -> Option<&Voxels> {
70
+ self.voxels.as_ref()
71
+ }
72
+ }
pack/state/space.rs ADDED
@@ -0,0 +1,98 @@
1
+ use std::collections::HashSet;
2
+
3
+ use bentopy::core::config::{CompartmentID, Quantity};
4
+
5
+ use crate::mask::{Dimensions, Mask};
6
+ use crate::session::{Locations, Session};
7
+ use crate::state::{Size, compartment::Compartment};
8
+
9
+ pub type Compartments = Vec<Compartment>;
10
+
11
+ pub struct Space {
12
+ pub size: Size,
13
+ pub dimensions: Dimensions,
14
+ pub resolution: f32,
15
+ pub compartments: Compartments,
16
+ pub periodic: bool,
17
+
18
+ pub(crate) global_background: Mask,
19
+ pub(crate) session_background: Mask,
20
+ /// The previous session's compartment IDs are used to see if a renewal of locations is
21
+ /// necessary between subsequent segment placements.
22
+ ///
23
+ /// When set to `None`, a renewal of locations is due for the next session, regardless of the
24
+ /// previous session's compartment IDs.
25
+ pub(crate) previous_compartments: Option<HashSet<CompartmentID>>,
26
+ }
27
+
28
+ impl Space {
29
+ pub fn enter_session<'s>(
30
+ &'s mut self,
31
+ compartment_ids: impl IntoIterator<Item = CompartmentID>,
32
+ locations: &'s mut Locations,
33
+ quantity: Quantity,
34
+ ) -> Session<'s> {
35
+ let compartment_ids = HashSet::from_iter(compartment_ids);
36
+
37
+ // TODO: Consider caching this volume like we do for the same compartments below.
38
+ // The volume can just be associated with a set of previous compartments.
39
+ let target = quantity.bake(|| self.volume(&compartment_ids));
40
+
41
+ // Set up a new session background if necessary.
42
+ // Otherwise, leave the session background and locations alone. The session background
43
+ // will stay exactly the same, since it was already set up for this set of
44
+ // compartments. The locations are likely still valid.
45
+ let same_previous_compartments = self
46
+ .previous_compartments
47
+ .as_ref()
48
+ .is_some_and(|prev| prev == &compartment_ids);
49
+ if !same_previous_compartments {
50
+ // Clone the global background, which has all structures stamped onto it.
51
+ self.session_background = self.global_background.clone();
52
+
53
+ // Apply the compartments to the background.
54
+ if let Some(merge) = self
55
+ .compartments
56
+ .iter()
57
+ .filter(|comp| compartment_ids.contains(&comp.id))
58
+ .map(|comp| comp.mask.clone())
59
+ .reduce(|mut acc, mask| {
60
+ acc.merge_mask(&mask);
61
+ acc
62
+ })
63
+ {
64
+ self.session_background.apply_mask(&merge);
65
+ }
66
+ self.previous_compartments = Some(compartment_ids);
67
+
68
+ // We must renew the locations as well, based on the newly masked session background.
69
+ locations.renew(self.session_background.linear_indices_where::<false>());
70
+ }
71
+
72
+ Session::new(
73
+ self,
74
+ locations,
75
+ target
76
+ .try_into()
77
+ .expect("target cannot not exceed system word size"),
78
+ )
79
+ }
80
+
81
+ /// Determine the free voxel volume for the specified compartments.
82
+ fn volume(&self, compartment_ids: &HashSet<CompartmentID>) -> f64 {
83
+ let free_voxels = self
84
+ .compartments
85
+ .iter()
86
+ .filter(|comp| compartment_ids.contains(&comp.id))
87
+ .map(|comp| comp.mask.clone())
88
+ .reduce(|mut acc, mask| {
89
+ acc.merge_mask(&mask);
90
+ acc
91
+ })
92
+ .map(|mask| mask.count::<false>())
93
+ .unwrap_or(0);
94
+
95
+ let voxel_volume = (self.resolution as f64).powi(3);
96
+ free_voxels as f64 * voxel_volume
97
+ }
98
+ }
pack/state.rs ADDED
@@ -0,0 +1,440 @@
1
+ use std::collections::HashMap;
2
+ use std::path::PathBuf;
3
+
4
+ use anyhow::{Context, bail};
5
+ use bentopy::core::config::{Config, defaults};
6
+ use bentopy::core::placement::{Meta, Placement, PlacementList};
7
+ pub(crate) use glam::{EulerRot, Mat3};
8
+ use rand::{RngCore, SeedableRng};
9
+
10
+ pub use bentopy::core::config;
11
+
12
+ use crate::args::{Args, RearrangeMethod};
13
+ use crate::mask::{Mask, distance_mask_grow};
14
+ pub use crate::state::compartment::Compartment;
15
+ use crate::state::segment::Segment;
16
+ pub use crate::state::space::Space;
17
+ use crate::structure::load_molecule;
18
+
19
+ mod combinations;
20
+ mod compartment;
21
+ mod mask;
22
+ mod pack;
23
+ mod segment;
24
+ mod space;
25
+
26
+ const ORDER: EulerRot = EulerRot::XYZ;
27
+
28
+ pub type Size = [f32; 3];
29
+ pub type Rotation = Mat3;
30
+ pub type Voxels = Mask;
31
+ pub type Rng = rand::rngs::StdRng; // TODO: Is this the fastest out there?
32
+
33
+ pub struct Output {
34
+ pub title: String,
35
+ pub path: PathBuf,
36
+ pub topol_includes: Vec<String>,
37
+ }
38
+
39
+ pub struct General {
40
+ pub seed: u64,
41
+ pub max_tries_multiplier: u64,
42
+ pub max_tries_per_rotation_divisor: u64,
43
+ pub bead_radius: f32,
44
+ }
45
+
46
+ pub struct State {
47
+ pub general: General,
48
+ pub space: Space,
49
+ pub segments: Vec<Segment>,
50
+ pub output: Output,
51
+
52
+ pub rng: Rng,
53
+ pub verbose: bool,
54
+ pub summary: bool,
55
+ }
56
+
57
+ impl State {
58
+ /// Set up the [`State`] given the [command line arguments](Args) and input file
59
+ /// [configuration](Config).
60
+ pub fn new(args: Args, config: Config) -> anyhow::Result<Self> {
61
+ // Read values from the general section of the config. If a command line argument is given,
62
+ // it overwrites the config value. (And the deprecated env vars have the highest priority.)
63
+
64
+ // If no seed is provided, use a random seed.
65
+ let seed = args
66
+ .seed
67
+ .or(config.general.seed)
68
+ .unwrap_or_else(|| Rng::from_os_rng().next_u64());
69
+ let rng = Rng::seed_from_u64(seed);
70
+
71
+ let bead_radius = args
72
+ .bead_radius
73
+ .or(config.general.bead_radius)
74
+ .unwrap_or(defaults::BEAD_RADIUS);
75
+
76
+ // Determine the max_tries parameters.
77
+ let max_tries_multiplier = if let Ok(s) = std::env::var("BENTOPY_TRIES") {
78
+ let n = s.parse().with_context(|| {
79
+ format!("Max tries multiplier should be a valid unsigned integer, found {s:?}")
80
+ })?;
81
+ eprintln!("\tMax tries multiplier set to {n}.");
82
+ eprintln!(
83
+ "\tWARNING: Setting max_tries_mult using the BENTOPY_TRIES environment variable will be deprecated."
84
+ );
85
+ eprintln!("\t Use --max-tries-mult instead.");
86
+ n
87
+ } else {
88
+ args.max_tries_mult
89
+ .or(config.general.max_tries_mult)
90
+ .unwrap_or(defaults::MAX_TRIES_MULT)
91
+ };
92
+
93
+ let max_tries_per_rotation_divisor = if let Ok(s) = std::env::var("BENTOPY_ROT_DIV") {
94
+ let n = s.parse().with_context(|| {
95
+ format!("Rotation divisor should be a valid unsigned integer, found {s:?}")
96
+ })?;
97
+ eprintln!("\tMax tries per rotation divisor set to {n}.");
98
+ eprintln!(
99
+ "\tWARNING: Setting max_tries_divisor using the BENTOPY_ROT_DIV environment variable will be deprecated."
100
+ );
101
+ eprintln!("\t Use --max-tries-rot-div instead.");
102
+ n
103
+ } else {
104
+ args.max_tries_rot_div
105
+ .or(config.general.max_tries_rot_div)
106
+ .unwrap_or(defaults::MAX_TRIES_ROT_DIV)
107
+ };
108
+
109
+ let verbose = args.verbose;
110
+
111
+ // Space.
112
+ // TODO: Consider if the resolution should be a default, again?
113
+ let resolution = config
114
+ .space
115
+ .resolution
116
+ .context("No resolution was specified in the input file")?
117
+ as f32;
118
+ let size = config
119
+ .space
120
+ .dimensions
121
+ .context("No dimensions were specified in the input file")?;
122
+ // The dimensions from the config is the real-space size of the box. Here, we treat the
123
+ // word dimensions as being the size in terms of voxels. Bit annoying.
124
+ // TODO: Reconsider this wording.
125
+ let dimensions = size.map(|d| (d / resolution) as u64);
126
+
127
+ eprintln!("Setting up compartments...");
128
+ let (predefined, combinations): (Vec<_>, Vec<_>) = config
129
+ .compartments
130
+ .into_iter()
131
+ .partition(config::Compartment::is_predefined);
132
+ let mut compartments: Vec<Compartment> = predefined
133
+ .into_iter()
134
+ .map(|comp| -> anyhow::Result<_> {
135
+ let mask = match comp.mask {
136
+ config::Mask::Voxels(path) => {
137
+ if verbose {
138
+ eprintln!("\tLoading mask from {path:?}...");
139
+ }
140
+ Mask::load_from_path(&path)
141
+ .with_context(|| format!("Failed to load mask {path:?}"))?
142
+ }
143
+ config::Mask::All => {
144
+ if verbose {
145
+ eprintln!("\tConstructing a full space mask...");
146
+ }
147
+ let shape = config::Shape::Cuboid {
148
+ start: config::Anchor::Start,
149
+ end: config::Anchor::End,
150
+ };
151
+ Mask::create_from_shape(dimensions, resolution, shape)
152
+ }
153
+ config::Mask::Shape(shape) => {
154
+ if verbose {
155
+ eprintln!("\tConstructing a {shape} mask...");
156
+ }
157
+ Mask::create_from_shape(dimensions, resolution, shape)
158
+ }
159
+ config::Mask::Limits(expr) => {
160
+ if verbose {
161
+ eprintln!("\tConstructing a mask from limits...");
162
+ }
163
+ compartment::distill_limits(&expr, dimensions, resolution as f64)
164
+ }
165
+ // We partitioned the list, so these variants are not present.
166
+ config::Mask::Within { .. } | config::Mask::Combination(_) => unreachable!(),
167
+ };
168
+
169
+ Ok(Compartment { id: comp.id, mask })
170
+ })
171
+ .collect::<anyhow::Result<_>>()?;
172
+ for combination in combinations {
173
+ if verbose {
174
+ eprintln!("\tApplying a compartment combination...");
175
+ }
176
+
177
+ let baked = match combination.mask {
178
+ config::Mask::Within { distance, id } => {
179
+ let compartment = compartments
180
+ .iter()
181
+ .find(|c| c.id == id)
182
+ .ok_or(anyhow::anyhow!("mask with id {id:?} not (yet) defined"))?;
183
+ let mask = &compartment.mask;
184
+ // TODO: This conversion appears correct but I'd like better reasoning. Consider.
185
+ let voxel_distance = (distance / resolution) as u64;
186
+ Compartment {
187
+ id: combination.id,
188
+ mask: distance_mask_grow(mask, voxel_distance),
189
+ }
190
+ }
191
+ config::Mask::Combination(expr) => Compartment {
192
+ id: combination.id,
193
+ mask: combinations::execute(&expr, &compartments)?,
194
+ },
195
+
196
+ // We partitioned the list, so these variants are not present.
197
+ config::Mask::All
198
+ | config::Mask::Voxels(_)
199
+ | config::Mask::Shape(_)
200
+ | config::Mask::Limits(_) => unreachable!(),
201
+ };
202
+
203
+ compartments.push(baked);
204
+ }
205
+ let space = Space {
206
+ size,
207
+ dimensions,
208
+ resolution,
209
+ compartments,
210
+ periodic: config.space.periodic.unwrap_or(defaults::PERIODIC),
211
+
212
+ global_background: Mask::new(dimensions),
213
+ session_background: Mask::new(dimensions),
214
+ previous_compartments: None,
215
+ };
216
+
217
+ // Segments.
218
+ eprintln!("Loading segment structures...");
219
+ let segments = {
220
+ let constraints: HashMap<&String, &config::Rule> = config
221
+ .constraints
222
+ .iter()
223
+ .map(|c| (&c.id, &c.rule))
224
+ .collect();
225
+ let mut segments: Vec<_> = config
226
+ .segments
227
+ .into_iter()
228
+ .map(|seg| -> Result<_, _> {
229
+ let path = seg.path;
230
+ if verbose {
231
+ eprintln!("\tLoading {path:?}...");
232
+ }
233
+ let name = seg.name;
234
+ let tag = seg.tag;
235
+ match tag.as_ref().map(String::len) {
236
+ Some(0) => eprintln!("WARNING: The tag for segment '{name}' is empty."),
237
+ Some(6.. ) => eprintln!("WARNING: The tag for segment '{name}' is longer than 5 characters, and may be truncated when the placement list is rendered."),
238
+ _ => {} // Nothing to warn about.
239
+ }
240
+ // TODO: Use the segment.name() method here? Or rather, it's better named
241
+ // future version ;)
242
+ let structure = load_molecule(&path).with_context(|| format!("Failed to open the structure file for segment '{name}' at {path:?}"))?;
243
+
244
+ // If there are any, there must be only one. We don't enforce that here though.
245
+ let rotation_axes = if let Some(id) = seg.rules.first() {
246
+ match constraints
247
+ .get(id)
248
+ .ok_or(anyhow::anyhow!("constraint with id {id:?} is not defined"))? {
249
+ config::Rule::RotationAxes(axes) => *axes,
250
+ }
251
+ } else {
252
+ config::Axes::default()
253
+ }
254
+ ;
255
+
256
+ Ok(Segment {
257
+ name,
258
+ tag,
259
+ quantity: seg.quantity,
260
+ compartments: seg.compartment_ids,
261
+ path,
262
+ rotation_axes,
263
+ structure,
264
+
265
+ // TODO: Entirely remove initial_rotation from the Segment struct?
266
+ initial_rotation: Rotation::IDENTITY,
267
+ rotation: Rotation::IDENTITY,
268
+ voxels: None,
269
+ })
270
+ })
271
+ .collect::<anyhow::Result<_>>()?;
272
+
273
+ let method = args.rearrange;
274
+ if let RearrangeMethod::None = method {
275
+ eprint!("Segments were not rearranged.");
276
+ } else {
277
+ eprint!("Rearranging segments according to the {method:?} method... ");
278
+ match method {
279
+ RearrangeMethod::Volume => {
280
+ segments
281
+ .iter_mut()
282
+ .for_each(|seg| seg.voxelize(space.resolution, bead_radius as f32));
283
+ segments.sort_by_cached_key(|seg| -> usize {
284
+ // We can safely unwrap because we just voxelized all segments.
285
+ seg.voxels().unwrap().count::<true>()
286
+ });
287
+ // TODO: Perhaps we can reverse _during_ the sorting operation with some trick?
288
+ segments.reverse();
289
+ }
290
+ RearrangeMethod::Moment => {
291
+ segments.sort_by_cached_key(|seg| {
292
+ (seg.structure.moment_of_inertia() * 1e6) as i64
293
+ });
294
+ // TODO: Perhaps we can reverse _during_ the sorting operation with some trick?
295
+ segments.reverse();
296
+ }
297
+
298
+ RearrangeMethod::BoundingSphere => {
299
+ segments.sort_by_cached_key(|seg| {
300
+ (seg.structure.bounding_sphere() * 1e6) as i64
301
+ });
302
+ // TODO: Perhaps we can reverse _during_ the sorting operation with some trick?
303
+ segments.reverse();
304
+ }
305
+ // Already taken care of above.
306
+ RearrangeMethod::None => {
307
+ unreachable!()
308
+ }
309
+ }
310
+ eprintln!("Done.");
311
+ }
312
+
313
+ segments
314
+ };
315
+
316
+ // Output.
317
+ let output = Output {
318
+ title: config.general.title.unwrap_or(defaults::TITLE.to_string()),
319
+ path: args.output,
320
+ // TODO: One more example of using a PathBuf here being a poor choice.
321
+ // TODO: Quite some cleanup here, eventually.
322
+ topol_includes: config
323
+ .includes
324
+ .into_iter()
325
+ .map(|p| p.to_string_lossy().to_string())
326
+ .collect(),
327
+ };
328
+
329
+ let general = General {
330
+ seed,
331
+ max_tries_multiplier,
332
+ max_tries_per_rotation_divisor,
333
+ bead_radius: bead_radius as f32,
334
+ };
335
+
336
+ Ok(Self {
337
+ general,
338
+ space,
339
+ segments,
340
+ output,
341
+
342
+ rng,
343
+ verbose,
344
+ summary: !args.no_summary,
345
+ })
346
+ }
347
+
348
+ pub fn check_masks(&self) -> anyhow::Result<()> {
349
+ if self.verbose {
350
+ eprintln!("Checking compartments...")
351
+ }
352
+
353
+ let mut checked = Vec::new(); // FIXME: BTreeMap?
354
+ for segment in &self.segments {
355
+ let ids = &segment.compartments;
356
+ let ids_formatted = ids.join(", ");
357
+ let name = &segment.name;
358
+ if ids.is_empty() {
359
+ // No compartments to check?!
360
+ unreachable!("a segment has at least one compartment")
361
+ }
362
+ if checked.contains(ids) {
363
+ // Already checked this rule.
364
+ continue;
365
+ }
366
+
367
+ // Check the masks.
368
+ let masks = self
369
+ .space
370
+ .compartments
371
+ .iter()
372
+ .filter(|c| ids.contains(&c.id))
373
+ .map(|c| &c.mask);
374
+ // TODO: Check for correctness.
375
+ let distilled = masks
376
+ .cloned()
377
+ .reduce(|mut acc, m| {
378
+ acc.merge_mask(&m);
379
+ acc
380
+ })
381
+ // We know there is at least one compartment.
382
+ // TODO: Don't we already check this upstream?
383
+ .ok_or(anyhow::anyhow!(
384
+ "no valid compartments ({ids_formatted}) declared for segment '{name}'"
385
+ ))?;
386
+
387
+ if self.verbose {
388
+ let n = distilled.count::<false>();
389
+ eprintln!("\t{n:>12} open voxels in compartments '{ids_formatted}'.")
390
+ }
391
+
392
+ if !distilled.any::<false>() {
393
+ // Provide some extra debug info about this failing segment's compartments if there
394
+ // is more than one. This is helpful in debugging problems if this error is hit.
395
+ if segment.compartments.len() > 1 {
396
+ eprintln!("\tIndividual compartments for segment '{name}':");
397
+ let compartments = self
398
+ .space
399
+ .compartments
400
+ .iter()
401
+ .filter(|c| ids.contains(&c.id));
402
+ for compartment in compartments {
403
+ let id = &compartment.id;
404
+ let n = compartment.mask.count::<false>();
405
+ eprintln!("\t{n:>12} open voxels in compartment '{id}'");
406
+ }
407
+ }
408
+ bail!(
409
+ "the compartments '{ids_formatted}' together preclude any placement of segment '{name}'"
410
+ );
411
+ }
412
+
413
+ checked.push(ids.clone())
414
+ }
415
+
416
+ if self.verbose {
417
+ let n = checked.len();
418
+ eprintln!("\tAll okay. Checked {n} segment compartment combinations.")
419
+ }
420
+
421
+ Ok(())
422
+ }
423
+
424
+ pub fn placement_list(&self, placements: impl IntoIterator<Item = Placement>) -> PlacementList {
425
+ let meta = Meta {
426
+ seed: self.general.seed,
427
+ max_tries_mult: self.general.max_tries_multiplier,
428
+ max_tries_per_rotation_divisor: self.general.max_tries_per_rotation_divisor,
429
+ bead_radius: self.general.bead_radius,
430
+ };
431
+
432
+ PlacementList {
433
+ title: self.output.title.to_string(),
434
+ size: self.space.size,
435
+ meta: Some(meta),
436
+ topol_includes: self.output.topol_includes.clone(),
437
+ placements: placements.into_iter().collect(),
438
+ }
439
+ }
440
+ }