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
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
|
+
}
|