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/session.rs ADDED
@@ -0,0 +1,176 @@
1
+ use glam::U64Vec3;
2
+ use rand::seq::SliceRandom;
3
+
4
+ use crate::Location;
5
+ use crate::mask::{Dimensions, Position};
6
+ use crate::state::{Rng, Space, Voxels};
7
+
8
+ pub struct Session<'s> {
9
+ inner: &'s mut Space,
10
+ locations: &'s mut Locations,
11
+ target: usize,
12
+ }
13
+
14
+ impl<'s> Session<'s> {
15
+ pub fn new(inner: &'s mut Space, locations: &'s mut Locations, target: usize) -> Self {
16
+ Self {
17
+ inner,
18
+ locations,
19
+ target,
20
+ }
21
+ }
22
+
23
+ pub fn target(&self) -> usize {
24
+ self.target
25
+ }
26
+ }
27
+
28
+ impl Session<'_> {
29
+ pub fn resolution(&self) -> f32 {
30
+ self.inner.resolution
31
+ }
32
+
33
+ pub fn dimensions(&self) -> Dimensions {
34
+ self.inner.dimensions
35
+ }
36
+
37
+ pub fn periodic(&self) -> bool {
38
+ self.inner.periodic
39
+ }
40
+
41
+ /// Returns a location if available.
42
+ ///
43
+ /// This function also takes a reference to a random number generator, since the internal
44
+ /// [`Locations`] type may shuffle its indices on demand.
45
+ ///
46
+ /// If the background for this [`Session`] does not have any free locations left, the session
47
+ /// must be ended. This case is indicated with a `None` return value.
48
+ pub fn pop_location(&mut self, rng: &mut Rng) -> Option<Location> {
49
+ // TODO: Consider having the shuffle guess that is passed to pop be dynamic with the number
50
+ // of placements that have already occurred. We could update that number as we go.
51
+ if let Some(location) = self.locations.pop(rng, self.target) {
52
+ return Some(location);
53
+ }
54
+
55
+ // We're out of locations. We'll try and renew once.
56
+ self.locations.renew(
57
+ self.inner
58
+ .session_background
59
+ .linear_indices_where::<false>(),
60
+ );
61
+
62
+ // If the result is still `None`, there is nothing we can do anymore.
63
+ self.locations.pop(rng, self.target)
64
+ }
65
+
66
+ /// Returns `true` if no collisions are encountered between the [`Space`] and the provided
67
+ /// [`Voxels`] at some `position`.
68
+ ///
69
+ /// If a collision is found, the function exits early with a `false` value.
70
+ pub fn check_collisions(&self, voxels: &Voxels, position: Position) -> bool {
71
+ let occupied_indices = voxels.indices_where::<true>().map(U64Vec3::from_array);
72
+ let position = U64Vec3::from_array(position);
73
+
74
+ occupied_indices
75
+ .map(|p| (p + position).to_array())
76
+ .all(|idx| !self.inner.session_background.get_periodic(idx))
77
+ }
78
+
79
+ pub fn stamp(&mut self, voxels: &Voxels, location: Position) {
80
+ let location = U64Vec3::from_array(location);
81
+ for idx in voxels
82
+ .indices_where::<true>()
83
+ .map(|idx| U64Vec3::from_array(idx) + location)
84
+ {
85
+ self.inner
86
+ .global_background
87
+ .set_periodic(idx.to_array(), true);
88
+ self.inner
89
+ .session_background
90
+ .set_periodic(idx.to_array(), true);
91
+ }
92
+ }
93
+
94
+ pub fn exit_session(&mut self) {
95
+ // Not really doing anything here anymore.
96
+ }
97
+
98
+ pub const fn position(&self, location: Location) -> Option<Position> {
99
+ self.inner.session_background.spatial_idx(location)
100
+ }
101
+ }
102
+
103
+ impl<'s> Drop for Session<'s> {
104
+ fn drop(&mut self) {
105
+ self.exit_session();
106
+ }
107
+ }
108
+
109
+ pub struct Locations {
110
+ /// Linear indices into the background map.
111
+ indices: Vec<Location>,
112
+ used: usize,
113
+ threshold: usize,
114
+
115
+ /// The number of shuffled elements that may be popped from the end of `indices`.
116
+ cursor: usize,
117
+ }
118
+
119
+ impl Locations {
120
+ /// Threshold of spare capacity at which the `locations` [`Vec`] ought to be shrunk.
121
+ const SHRINK_THRESHOLD: usize = (100 * 1024_usize.pow(2)) / std::mem::size_of::<Location>();
122
+ /// Value that determines the threshold at which the locations will be renewed.
123
+ const RENEW_THRESHOLD: f64 = 0.1;
124
+
125
+ pub fn new() -> Self {
126
+ Self {
127
+ indices: Vec::new(),
128
+ used: 0,
129
+ threshold: 0,
130
+ cursor: 0,
131
+ }
132
+ }
133
+
134
+ /// Renew the locations from an iterator of locations.
135
+ pub fn renew(&mut self, locations: impl Iterator<Item = Location>) {
136
+ let indices = &mut self.indices;
137
+ indices.clear();
138
+ indices.extend(locations);
139
+ // Shrink the `locations` if the spare capacity is getting out of hand.
140
+ if indices.spare_capacity_mut().len() > Self::SHRINK_THRESHOLD {
141
+ indices.shrink_to_fit();
142
+ }
143
+ self.used = 0;
144
+ self.cursor = 0;
145
+ // We want to make sure that the threshold is rounded down, such that it eventually could
146
+ // reach 0, indicating the enclosing Session is exhausted. See `pop`.
147
+ self.threshold = (indices.len() as f64 * Self::RENEW_THRESHOLD).floor() as usize
148
+ }
149
+
150
+ fn shuffle(&mut self, rng: &mut Rng, shuffle_guess: usize) {
151
+ let (shuffled, _unshuffled) = self.indices.partial_shuffle(rng, shuffle_guess);
152
+ self.cursor = shuffled.len();
153
+ }
154
+
155
+ /// Pop a location.
156
+ ///
157
+ /// This function also takes a reference to a random number generator, since this type may
158
+ /// shuffle its indices on demand. The `shuffle_guess` suggests the number of items that ought
159
+ /// to be shuffled in that scenario.
160
+ ///
161
+ /// A `None` value indicates that this [`Locations`] object must be [renewed](`Self::renew`),
162
+ /// not necessarily that all locations have been popped.
163
+ pub fn pop(&mut self, rng: &mut Rng, shuffle_guess: usize) -> Option<Location> {
164
+ if self.used >= self.threshold {
165
+ return None; // Indicate a renewal is needed.
166
+ }
167
+ if self.cursor == 0 {
168
+ self.shuffle(rng, shuffle_guess);
169
+ }
170
+ let location = self.indices.pop()?;
171
+ self.used += 1;
172
+ assert!(self.cursor > 0);
173
+ self.cursor -= 1;
174
+ Some(location)
175
+ }
176
+ }
@@ -0,0 +1,31 @@
1
+ use bentopy::core::config::{CompartmentID, Expr};
2
+
3
+ use crate::{mask::Mask, state::Compartment};
4
+
5
+ pub fn execute(expr: &Expr<CompartmentID>, compartments: &[Compartment]) -> anyhow::Result<Mask> {
6
+ let output = match expr {
7
+ Expr::Term(id) => {
8
+ let compartment = compartments
9
+ .iter()
10
+ .find(|c| &c.id == id)
11
+ .ok_or(anyhow::anyhow!("mask with id {id:?} not (yet) defined"))?;
12
+ compartment.mask.clone()
13
+ }
14
+ Expr::Not(expr) => !execute(expr, compartments)?,
15
+ // TODO: This is quite a horrible, no-good transliteration of the legacy_execute function.
16
+ // There is some much here that can be improved upon, in terms of memory efficiency, for
17
+ // example.
18
+ Expr::Or(lhs, rhs) => {
19
+ let mut m = execute(lhs, compartments)?;
20
+ m &= execute(rhs, compartments)?;
21
+ m
22
+ }
23
+ Expr::And(lhs, rhs) => {
24
+ let mut m = execute(lhs, compartments)?;
25
+ m |= execute(rhs, compartments)?;
26
+ m
27
+ }
28
+ };
29
+
30
+ Ok(output)
31
+ }
@@ -0,0 +1,44 @@
1
+ use bentopy::core::config::{CompartmentID, Expr, Limit};
2
+
3
+ use crate::mask::{Dimensions, Mask};
4
+
5
+ pub struct Compartment {
6
+ pub id: CompartmentID,
7
+ pub mask: Mask,
8
+ }
9
+
10
+ pub fn distill_limits(expr: &Expr<Limit>, dimensions: Dimensions, resolution: f64) -> Mask {
11
+ fn apply_limit(mut mask: Mask, limit: &Limit, resolution: f64) -> Mask {
12
+ let &Limit { axis, op, value } = limit;
13
+ match op {
14
+ bentopy::core::config::Op::LessThan => {
15
+ mask.apply_function(|pos| value < pos[axis as usize] as f64 * resolution)
16
+ }
17
+ bentopy::core::config::Op::GreaterThan => {
18
+ mask.apply_function(|pos| value > pos[axis as usize] as f64 * resolution)
19
+ }
20
+ }
21
+
22
+ mask
23
+ }
24
+
25
+ // TODO: This is naive. Also wrong perhaps. Let's test this thuroughly.
26
+ fn d(expr: &Expr<Limit>, dimensions: Dimensions, resolution: f64) -> Mask {
27
+ match expr {
28
+ Expr::Term(limit) => apply_limit(Mask::new(dimensions), limit, resolution),
29
+ Expr::Not(expr) => !d(expr, dimensions, resolution),
30
+ Expr::Or(lhs, rhs) => {
31
+ let mut m = d(lhs, dimensions, resolution);
32
+ m &= d(rhs, dimensions, resolution);
33
+ m
34
+ }
35
+ Expr::And(lhs, rhs) => {
36
+ let mut m = d(lhs, dimensions, resolution);
37
+ m |= d(rhs, dimensions, resolution);
38
+ m
39
+ }
40
+ }
41
+ }
42
+
43
+ d(expr, dimensions, resolution)
44
+ }
pack/state/mask.rs ADDED
@@ -0,0 +1,196 @@
1
+ use std::path::Path;
2
+
3
+ use anyhow::{Context, Result, bail};
4
+ use bentopy::core::config::{self, legacy::Shape as LegacyConfigShape};
5
+ use glam::{U64Vec3, UVec3, Vec3};
6
+
7
+ use crate::mask::{Dimensions, Mask};
8
+
9
+ impl Mask {
10
+ /// All dimensions in terms of voxels.
11
+ pub(crate) fn legacy_create_from_shape(
12
+ shape: LegacyConfigShape,
13
+ dimensions: Dimensions,
14
+ center: Option<UVec3>,
15
+ radius: Option<u32>,
16
+ ) -> Self {
17
+ let [w, h, d] = dimensions.map(|v| v as usize);
18
+ let min_dim = dimensions.into_iter().min().unwrap() as u32; // Has non-zero length.
19
+ let r = radius.unwrap_or(min_dim / 2);
20
+ assert!(
21
+ min_dim >= r,
22
+ "the provided radius ({r} voxels) must not exceed the space's smallest dimension ({min_dim} voxels)"
23
+ );
24
+ let c = center.unwrap_or(UVec3::splat(r)).as_ivec3();
25
+ assert!(U64Vec3::from_array(dimensions).cmpge(c.as_u64vec3()).all());
26
+ let r2 = r.pow(2);
27
+
28
+ // TODO: We can use some nice iterator tricks to avoid allocating a big Vec<bool> here.
29
+ // TODO: Profile to see whether we can help the inlining along.
30
+ let mut cells = Vec::with_capacity(w * h * d);
31
+ for z in 0..d as i32 {
32
+ for y in 0..h as i32 {
33
+ for x in 0..w as i32 {
34
+ let cell = match shape {
35
+ LegacyConfigShape::Spherical => {
36
+ // TODO: Profile and inspect asm to see whether this inlines well.
37
+ ((x - c.x).pow(2) as u32)
38
+ + ((y - c.y).pow(2) as u32)
39
+ + ((z - c.z).pow(2) as u32)
40
+ >= r2
41
+ }
42
+ LegacyConfigShape::Cuboid | LegacyConfigShape::None => false,
43
+ };
44
+ cells.push(cell);
45
+ }
46
+ }
47
+ }
48
+ let cells = cells.into_boxed_slice();
49
+
50
+ Self::from_cells(dimensions, &cells)
51
+ }
52
+
53
+ // TODO: Still shitty version. Will be better with a rewrite.
54
+ pub(crate) fn create_cuboid(
55
+ dimensions: [u64; 3],
56
+ resolution: f32,
57
+ start: config::Anchor,
58
+ end: config::Anchor,
59
+ ) -> Self {
60
+ let [w, h, d] = dimensions.map(|v| v as usize);
61
+ let dimensions = U64Vec3::from_array(dimensions);
62
+
63
+ let start = match start {
64
+ config::Anchor::Start => U64Vec3::ZERO,
65
+ config::Anchor::Center => dimensions / 2,
66
+ config::Anchor::End => dimensions,
67
+ config::Anchor::Point(point) => (Vec3::from(point) / resolution).as_u64vec3(),
68
+ };
69
+ let end = match end {
70
+ config::Anchor::Start => U64Vec3::ZERO,
71
+ config::Anchor::Center => dimensions / 2,
72
+ config::Anchor::End => dimensions,
73
+ config::Anchor::Point(point) => (Vec3::from(point) / resolution).as_u64vec3(),
74
+ };
75
+ // TODO: Consider if we want to do this here, or make the correct ordering an invariant.
76
+ let (start, end) = (U64Vec3::min(start, end), U64Vec3::max(start, end));
77
+
78
+ assert!(
79
+ dimensions.cmpge(start).all(),
80
+ "start ({start}) must be within the space dimensions ({dimensions})"
81
+ );
82
+ assert!(
83
+ dimensions.cmpge(end).all(),
84
+ "end ({end}) must be within the space dimensions ({dimensions})"
85
+ );
86
+
87
+ // TODO: We can use some nice iterator tricks to avoid allocating a big Vec<bool> here.
88
+ // TODO: Profile to see whether we can help the inlining along.
89
+ let mut cells = vec![true; w * h * d];
90
+ for z in 0..d {
91
+ for y in 0..h {
92
+ for x in 0..w {
93
+ let idx = x + w * y + w * h * z;
94
+ let free = (start.x..end.x).contains(&(x as u64))
95
+ && (start.y..end.y).contains(&(y as u64))
96
+ && (start.z..end.z).contains(&(z as u64));
97
+ cells[idx] = !free;
98
+ }
99
+ }
100
+ }
101
+ let cells = cells.into_boxed_slice();
102
+
103
+ Self::from_cells(dimensions.into(), &cells)
104
+
105
+ // TODO: Go with something along these lines. Likely faster and more memory efficient.
106
+ // let mut mask = Self::new(dimensions);
107
+ // mask.apply_function(|[x,y,z]| );
108
+ }
109
+
110
+ pub(crate) fn create_from_shape(
111
+ dimensions: [u64; 3],
112
+ resolution: f32,
113
+ shape: config::Shape,
114
+ ) -> Mask {
115
+ // TODO: The current implementation just giving the helm over to legacy_create_from_shape
116
+ // is very sad and will be corrected.
117
+ match shape {
118
+ config::Shape::Sphere { center, radius } => {
119
+ let center = match center {
120
+ config::Anchor::Center => (U64Vec3::from(dimensions) / 2).as_uvec3(),
121
+ config::Anchor::Point(point) => (Vec3::from(point) / resolution).as_uvec3(),
122
+ config::Anchor::Start => UVec3::ZERO,
123
+ config::Anchor::End => U64Vec3::from(dimensions).as_uvec3(),
124
+ };
125
+ let radius = (radius / resolution) as u32;
126
+ Self::legacy_create_from_shape(
127
+ LegacyConfigShape::Spherical,
128
+ dimensions,
129
+ Some(center),
130
+ Some(radius),
131
+ )
132
+ }
133
+ config::Shape::Cuboid { start, end } => {
134
+ Self::create_cuboid(dimensions, resolution, start, end)
135
+ }
136
+ }
137
+ // LegacyConfigMask::Analytical {
138
+ // shape,
139
+ // center,
140
+ // radius,
141
+ // } => {
142
+ // if verbose {
143
+ // eprintln!("\tConstructing a {shape} mask...");
144
+ // }
145
+ // let center = center.map(|c| (Vec3::from_array(c) / resolution).as_uvec3());
146
+ // let radius = radius.map(|r| (r / resolution) as u32);
147
+ // Mask::legacy_create_from_shape(shape, dimensions, center, radius)
148
+ // }
149
+ }
150
+
151
+ pub(crate) fn load_from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
152
+ let mut npz = npyz::npz::NpzArchive::open(path)?;
153
+ // FIXME: This error could be handled more gracefully.
154
+ let first_name = npz
155
+ .array_names()
156
+ .next()
157
+ .context("There should be at least one array in the mask voxel map")?
158
+ .to_string();
159
+ let array = npz.by_name(&first_name)?.unwrap(); // We just asserted the name exists.
160
+ let Ok(size): Result<[u64; 3], _> = array.shape().to_vec().try_into() else {
161
+ let shape = array.shape();
162
+ bail!("a voxel map must have three dimensions, found {shape:?}");
163
+ };
164
+ if size.iter().any(|d| d == &0) {
165
+ bail!("a voxel map must have non-zero sized dimensions, found {size:?}")
166
+ }
167
+
168
+ let order = array.order();
169
+ let mut cells: Box<[bool]> = array.into_vec()?.into_boxed_slice();
170
+ cells.iter_mut().for_each(|cell| *cell = !(*cell));
171
+ let cells = match order {
172
+ npyz::Order::C => {
173
+ // We need to re-order the cells to get them into the expected Fortran ordering.
174
+ let mut reordered = Vec::with_capacity(cells.len());
175
+ reordered.resize(cells.len(), false);
176
+ let mut cells = IntoIterator::into_iter(cells);
177
+ let mut reordered = reordered.into_boxed_slice();
178
+ let [w, h, d] = size;
179
+ assert_eq!(cells.len() as u64, w * h * d);
180
+ for x in 0..w {
181
+ for y in 0..h {
182
+ for z in 0..d {
183
+ // We know that cells and reordered have the same size, and we have
184
+ // asserted that `size` corresponds to the same length.
185
+ reordered[(x + y * w + z * w * h) as usize] = cells.next().unwrap();
186
+ }
187
+ }
188
+ }
189
+ reordered
190
+ }
191
+ npyz::Order::Fortran => cells,
192
+ };
193
+
194
+ Ok(Self::from_cells(size, &cells))
195
+ }
196
+ }
pack/state/pack.rs ADDED
@@ -0,0 +1,187 @@
1
+ use std::io;
2
+
3
+ pub(crate) use glam::Mat3;
4
+ use rand::Rng as _;
5
+
6
+ use crate::session::Locations;
7
+ use crate::state::State;
8
+ use crate::{CLEAR_LINE, Summary};
9
+ use bentopy::core::placement::{Batch, Placement};
10
+
11
+ impl State {
12
+ pub fn pack(&mut self, log: &mut impl io::Write) -> anyhow::Result<(Vec<Placement>, Summary)> {
13
+ let start = std::time::Instant::now();
14
+ let mut locations = Locations::new();
15
+ let mut placements = Vec::new();
16
+ let mut summary = Summary::new();
17
+
18
+ let state = self;
19
+ let n_segments = state.segments.len();
20
+ for (i, segment) in state.segments.iter_mut().enumerate() {
21
+ if segment.quantity.is_zero() {
22
+ write!(
23
+ log,
24
+ "{prefix}({i:>3}/{n_segments}) Skipping attempt to pack 0 instances of segment '{name}'.{suffix}",
25
+ prefix = if state.verbose { "" } else { CLEAR_LINE },
26
+ i = i + 1,
27
+ name = segment.name,
28
+ suffix = if state.verbose { "\n" } else { "" }
29
+ )?;
30
+ continue;
31
+ }
32
+
33
+ write!(
34
+ log,
35
+ "{prefix}({i:>3}/{n_segments}) Attempting to pack {target:>5} instances of segment '{name}'.{suffix}",
36
+ prefix = if state.verbose { "" } else { CLEAR_LINE },
37
+ i = i + 1,
38
+ target = segment.quantity,
39
+ name = segment.name,
40
+ suffix = if state.verbose { "\n" } else { "" }
41
+ )?;
42
+
43
+ // Prepare the session.
44
+ let start_session = std::time::Instant::now();
45
+ let mut session = state.space.enter_session(
46
+ segment.compartments.iter().cloned(),
47
+ &mut locations,
48
+ segment.quantity,
49
+ );
50
+
51
+ // Set up the placement record for this segment.
52
+ let mut placement = Placement::new(
53
+ segment.name.clone(),
54
+ segment.tag.clone(),
55
+ segment.path.clone(),
56
+ );
57
+
58
+ let mut hits = 0;
59
+ let mut tries = 0; // The number of unsuccessful tries.
60
+ let mut tries_per_rotation = 0;
61
+ // The maximum number of unsuccessful tries.
62
+ let max_tries = state.general.max_tries_multiplier * session.target() as u64;
63
+ // The number of
64
+ let max_tries_per_rotation = max_tries / state.general.max_tries_per_rotation_divisor;
65
+ let mut batch_positions = Vec::new();
66
+ 'placement: while hits < session.target() {
67
+ if tries >= max_tries {
68
+ if state.verbose {
69
+ writeln!(log, "Exiting after {tries} unsuccessful tries.")?;
70
+ }
71
+ break 'placement; // "When you try your best, but you don't succeed."
72
+ }
73
+
74
+ // TODO: This can become more efficient for successive placement failures.
75
+ // TODO: Also, this should become a method on Segment.
76
+ let resolution = session.resolution();
77
+ let voxels = match segment.voxels() {
78
+ Some(voxels) => voxels,
79
+ None => {
80
+ segment.voxelize(resolution, state.general.bead_radius);
81
+ segment.voxels().unwrap() // We just voxelized.
82
+ }
83
+ };
84
+
85
+ // FIXME: Do all this math with glam::U64Vec?
86
+ let [bx, by, bz] = session.dimensions();
87
+ let [sx, sy, sz] = voxels.dimensions();
88
+ if sx > bx || sy > by || sz > bz {
89
+ tries += 1;
90
+
91
+ // What about another rotation?
92
+ let rotation = Mat3::from_quat(state.rng.random());
93
+ segment.set_rotation(rotation);
94
+ tries_per_rotation = 0; // Reset the counter.
95
+ if state.verbose {
96
+ eprintln!("\tNew rotation. The previous rotation would never have fit.")
97
+ }
98
+
99
+ continue 'placement; // Segment voxels size exceeds the size of the background.
100
+ }
101
+ let [maxx, maxy, maxz] = [bx - sx, by - sy, bz - sz];
102
+
103
+ // Pick a random location.
104
+ let position = 'location: loop {
105
+ let candidate = match session.pop_location(&mut state.rng) {
106
+ Some(location) => location,
107
+ None => {
108
+ // We've gone through all the locations.
109
+ // TODO: This is rather unlikely, but must be dealt with correctly. I think
110
+ // this may require a nice custom type that does some internal mutabilty
111
+ // trickery.
112
+ // For now, we'll just break here.
113
+ break 'placement;
114
+ }
115
+ };
116
+
117
+ // Convert the linear index location to a spatial index.
118
+ let position = session.position(candidate).unwrap();
119
+
120
+ // If we are not packing in a periodic manner, we skip candidate positions that
121
+ // would necessitate periodic behavior. When a location passes this point, it is
122
+ // valid for non-periodic placement.
123
+ if !session.periodic() {
124
+ // Check if the segment will fit in the background, given this position.
125
+ let [x, y, z] = position;
126
+ if x >= maxx || y >= maxy || z >= maxz {
127
+ continue 'location;
128
+ }
129
+ }
130
+
131
+ // All seems good, so we continue to see if this position will be accepted.
132
+ break position;
133
+ };
134
+
135
+ // Reject if it would cause a collision.
136
+ if !session.check_collisions(voxels, position) {
137
+ tries += 1;
138
+ tries_per_rotation += 1;
139
+ if tries_per_rotation >= max_tries_per_rotation {
140
+ let rotation = Mat3::from_quat(state.rng.random());
141
+ segment.set_rotation(rotation);
142
+ tries_per_rotation = 0; // Reset the counter.
143
+ }
144
+ continue 'placement; // Reject due to collision.
145
+ }
146
+
147
+ // We found a good spot. Stomp on those stamps!
148
+ session.stamp(voxels, position);
149
+ // Transform the location to nm.
150
+ batch_positions.push(position.map(|v| v as f32 * session.resolution()));
151
+
152
+ // Let's write out the batch and rotate the segment again.
153
+ // TODO: Perhaps we'll need a little transpose here.
154
+ let batch = Batch::new(segment.rotation(), batch_positions.clone());
155
+ placement.push(batch);
156
+ batch_positions.clear();
157
+
158
+ let rotation = Mat3::from_quat(state.rng.random());
159
+ segment.set_rotation(rotation);
160
+ tries_per_rotation = 0; // Reset the counter.
161
+
162
+ hits += 1;
163
+ }
164
+ let duration = start_session.elapsed().as_secs_f64();
165
+
166
+ if state.verbose {
167
+ let total = start.elapsed().as_secs();
168
+ writeln!(
169
+ log,
170
+ " Packed {hits:>5} instances in {duration:6.3} s. [{total} s] <{:.4} of max_tries, {:.4} of target>",
171
+ tries as f32 / max_tries as f32,
172
+ tries as f32 / session.target() as f32
173
+ )?;
174
+ }
175
+
176
+ // Save the batches.
177
+ summary.push(segment.name.clone(), session.target(), hits, duration);
178
+ placements.push(placement);
179
+ }
180
+
181
+ if !state.verbose {
182
+ writeln!(log)?; // Go down one line to prevent overwriting the last line.
183
+ }
184
+
185
+ Ok((placements, summary))
186
+ }
187
+ }