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
solvate/solvate.rs ADDED
@@ -0,0 +1,244 @@
1
+ use eightyseven::structure::ResName;
2
+ use glam::{UVec3, Vec3};
3
+
4
+ use crate::convert::Convert;
5
+ use crate::cookies::Cookies;
6
+ use crate::placement::PlaceMap;
7
+ use crate::placement::iter_3d;
8
+ use crate::structure::{BoxVecsExtension, Structure};
9
+ use crate::water::Water;
10
+ use crate::{BoundaryMode, PeriodicMode};
11
+
12
+ /// Solvate a [`Structure`] with a template solvent box.
13
+ #[allow(clippy::too_many_arguments)]
14
+ pub fn solvate<'sol>(
15
+ structure: &mut Structure,
16
+ solvent: Water,
17
+ cutoff: f32,
18
+ solvent_cutoff: f32,
19
+ ignore: &[ResName],
20
+ center: bool,
21
+ boundary_mode: BoundaryMode,
22
+ periodic_mode: PeriodicMode,
23
+ ) -> PlaceMap<'sol> {
24
+ // Determine how many copies of the solvent cell are required to fill the input box for each
25
+ // direction.
26
+ let cookie_size = solvent.dimensions();
27
+ let d = structure.boxvecs.as_vec3() / cookie_size;
28
+ let dimensions = d.ceil().as_uvec3();
29
+
30
+ // Depending on the desired behavior, modify the structure.
31
+ match boundary_mode {
32
+ BoundaryMode::Cut => {}
33
+ BoundaryMode::Grow => {
34
+ eprintln!("\tGrowing output structure box to fit solvent precisely.");
35
+ // If the solvent box does not fit precisely (viz., the input box size is not an
36
+ // integer multiple of the solvent box size), the size will be overshot. This means that in
37
+ // many cases, the size of the output box may be greater than that of the input structure.
38
+ let remainder = d.fract() * cookie_size;
39
+ if center {
40
+ // Add half of the overshot to the positions in the structure to place it in the center of
41
+ // the final box.
42
+ let offset = remainder * 0.5;
43
+ for bead in &mut structure.atoms {
44
+ bead.position += offset.convert();
45
+ }
46
+
47
+ eprintln!("\tCentered structure in expanded box.");
48
+ }
49
+
50
+ match &mut structure.boxvecs {
51
+ // We don't have to cut off the solvent structures at the boundaries, but we have to
52
+ // change the boundaries to fit.
53
+ eightyseven::structure::BoxVecs::Short(three) => {
54
+ *three = (Vec3::from_array(*three) + remainder).to_array()
55
+ }
56
+ eightyseven::structure::BoxVecs::Full(_) => todo!(),
57
+ }
58
+ }
59
+ }
60
+
61
+ // Cut the input structure into cell-sized cookies that are ordered in the same manner as the
62
+ // solvent placement map is.
63
+ if !ignore.is_empty() {
64
+ let ignored = ignore.join(", ");
65
+ eprintln!("\tParticles with the following resnames will be ignored: {ignored}");
66
+ }
67
+ eprint!("\tCutting cookies... ");
68
+ let start = std::time::Instant::now();
69
+ let cookies = Cookies::new(
70
+ structure,
71
+ ignore,
72
+ cookie_size,
73
+ dimensions,
74
+ cutoff,
75
+ periodic_mode,
76
+ );
77
+ eprintln!("Took {:.3} s.", start.elapsed().as_secs_f32());
78
+
79
+ // For each location of a solvent box, go through each of the solvent bead positions and see
80
+ // whether it collides with a structure atom. If it does, we jot that down so we don't write
81
+ // that solvent bead out later.
82
+ eprint!("\tChecking solvent collisions... ");
83
+ let start = std::time::Instant::now();
84
+ let mut placemap = PlaceMap::new(solvent, dimensions);
85
+ let cutoff2 = cutoff.powi(2); // Square to avoid taking the square root of the distances.
86
+ for cell_pos in iter_3d(dimensions) {
87
+ let cookie = cookies.get(cell_pos).unwrap(); // We are sure there is a cookie here.
88
+ if cookie.is_empty() {
89
+ // If the cookie does not contain any structure beads, we can just skip
90
+ // measuring any distances.
91
+ continue;
92
+ }
93
+ let translation = cookies.offset(cell_pos);
94
+ // TODO: This is a bit dirty but necessary for the time being because we are iterating over
95
+ // something that we also need to modify.
96
+ let p = placemap.all_positions().to_vec();
97
+ for (idx, &solvent_bead) in p.iter().enumerate() {
98
+ let solvent_pos = solvent_bead + translation;
99
+ let mut collision = false;
100
+ for &cookie_bead in cookie {
101
+ // TODO: Consider applying this translation as a map before the iter over
102
+ // the solvent beads?
103
+ // Translate the unit cell solvent bead position to the reference frame of
104
+ // the bead from the cookie.
105
+ if solvent_pos.distance_squared(cookie_bead) < cutoff2 {
106
+ collision = true;
107
+ break;
108
+ }
109
+ }
110
+ if collision {
111
+ // This unwrap is safe, since we are sure there is a cell here.
112
+ let placement = &mut placemap.get_mut(cell_pos).unwrap();
113
+ placement.set_occupied(idx)
114
+ }
115
+ }
116
+ }
117
+ eprintln!("Took {:.3} s.", start.elapsed().as_secs_f32());
118
+
119
+ // Deal with the boundary conditions.
120
+ match boundary_mode {
121
+ BoundaryMode::Cut => {
122
+ let solvent_cutoff2 = solvent_cutoff.powi(2);
123
+
124
+ eprint!("\tCutting solvent to fit box... ");
125
+ let start = std::time::Instant::now();
126
+ for axis in [Axis::X, Axis::Y, Axis::Z] {
127
+ let max = dimensions - 1;
128
+ let box_dimensions = structure.boxvecs.as_vec3();
129
+ let (main, asz, bsz) = match axis {
130
+ Axis::X => (Vec3::X, dimensions.y, dimensions.z),
131
+ Axis::Y => (Vec3::Y, dimensions.x, dimensions.z),
132
+ Axis::Z => (Vec3::Z, dimensions.x, dimensions.y),
133
+ };
134
+
135
+ // Set up a 3×3 layer of atoms as a crown on the box dimensions at the `this` box.
136
+ let limit = axis.select(box_dimensions);
137
+ let crown: Box<[Vec3]> = axis
138
+ .crown(box_dimensions)
139
+ .map(|translation| {
140
+ placemap
141
+ .all_positions()
142
+ .iter()
143
+ .map(move |&a| a + translation)
144
+ // We are only interested in positions that lie at the periodic
145
+ // interface. Anything beyond the solvent cutoff distance from that
146
+ // interface is out of reach.
147
+ .filter(|&position| axis.select(position) <= limit + solvent_cutoff)
148
+ })
149
+ .into_iter()
150
+ .flatten()
151
+ .collect();
152
+
153
+ let translation = cookies.offset(main.as_uvec3() * max);
154
+ let this = placemap
155
+ .all_positions()
156
+ .iter()
157
+ .map(|&position| position + translation);
158
+
159
+ // We'll make a list of solvent atoms we need to reject by index.
160
+ let rejected: Box<[_]> = this
161
+ .enumerate()
162
+ .filter_map(|(idx, bead)| {
163
+ let outside = axis.select(bead) > limit;
164
+ let too_close = crown
165
+ .iter()
166
+ .any(|&crown_bead| bead.distance_squared(crown_bead) < solvent_cutoff2);
167
+ if outside || too_close {
168
+ Some(idx)
169
+ } else {
170
+ None
171
+ }
172
+ })
173
+ .collect();
174
+
175
+ // Apply the rejections to all solvent boxes on this face.
176
+ for b in 0..bsz {
177
+ for a in 0..asz {
178
+ let pos = match axis {
179
+ Axis::X => UVec3::new(max.x, a, b),
180
+ Axis::Y => UVec3::new(a, max.y, b),
181
+ Axis::Z => UVec3::new(a, b, max.z),
182
+ };
183
+ let mut pm = placemap.get_mut(pos).unwrap();
184
+ for &idx in &rejected {
185
+ pm.set_occupied(idx)
186
+ }
187
+ }
188
+ }
189
+ }
190
+ eprintln!("Took {:.6} s.", start.elapsed().as_secs_f32());
191
+ }
192
+ // We already set the box size at the top. Nothing to do here.
193
+ BoundaryMode::Grow => {}
194
+ }
195
+
196
+ placemap
197
+ }
198
+
199
+ #[derive(Clone, Copy)]
200
+ enum Axis {
201
+ X,
202
+ Y,
203
+ Z,
204
+ }
205
+
206
+ impl Axis {
207
+ const fn select(&self, v: Vec3) -> f32 {
208
+ v.to_array()[*self as usize]
209
+ }
210
+
211
+ const fn roll(&self, mut v: Vec3) -> Vec3 {
212
+ let mut n = 0;
213
+ while n < *self as usize {
214
+ v = Vec3 {
215
+ x: v.z,
216
+ y: v.x,
217
+ z: v.y,
218
+ };
219
+ n += 1;
220
+ }
221
+ v
222
+ }
223
+
224
+ const fn crown(&self, box_dimensions: Vec3) -> [Vec3; 9] {
225
+ let d = self.select(box_dimensions);
226
+ let mut crown = [
227
+ Vec3::new(d, -1.0, -1.0),
228
+ Vec3::new(d, 0.0, -1.0),
229
+ Vec3::new(d, 1.0, -1.0),
230
+ Vec3::new(d, -1.0, 0.0),
231
+ Vec3::new(d, 0.0, 0.0),
232
+ Vec3::new(d, 1.0, 0.0),
233
+ Vec3::new(d, -1.0, 1.0),
234
+ Vec3::new(d, 0.0, 1.0),
235
+ Vec3::new(d, 1.0, 1.0),
236
+ ];
237
+ let mut i = 0;
238
+ while i < crown.len() {
239
+ crown[i] = self.roll(crown[i]);
240
+ i += 1;
241
+ }
242
+ crown
243
+ }
244
+ }
solvate/structure.rs ADDED
@@ -0,0 +1,160 @@
1
+ use std::io;
2
+
3
+ use eightyseven::structure::BoxVecs;
4
+ use eightyseven::writer::WriteGro;
5
+ use glam::Vec3;
6
+ use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
7
+
8
+ use crate::placement::PlaceMap;
9
+ use crate::substitute::Substitution;
10
+
11
+ pub type Structure = eightyseven::structure::Structure;
12
+
13
+ pub trait BoxVecsExtension {
14
+ fn as_vec3(&self) -> glam::Vec3;
15
+ }
16
+
17
+ impl BoxVecsExtension for BoxVecs {
18
+ fn as_vec3(&self) -> Vec3 {
19
+ match self {
20
+ // FIX: When a gro file with cuboid boundaries represented as a nine-valued vector
21
+ // (trailing six values are zero) is loaded, we can canonicalize it as a three-valued
22
+ // cuboid. This should probably be fixed in eightyseven, but I'll want to reconsider
23
+ // this and related issues properly at another moment.
24
+ &BoxVecs::Short(three) | &BoxVecs::Full([three @ .., 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) => {
25
+ Vec3::from_array(three)
26
+ }
27
+ BoxVecs::Full(nine) => todo!("nine-value boxvecs ({nine:?}) are currently unsupported"),
28
+ }
29
+ }
30
+ }
31
+
32
+ pub fn write_structure<const PAR: bool>(
33
+ writer: &mut impl io::Write,
34
+ structure: &Structure,
35
+ solvent_placemap: &PlaceMap,
36
+ substitutions: &[Substitution],
37
+ buffer_size: usize,
38
+ ) -> io::Result<()> {
39
+ let solvent = solvent_placemap.solvent;
40
+ let natoms_structure = structure.natoms();
41
+ let natoms_per_residue = solvent.residue_points();
42
+ let natoms_solvent = solvent_placemap.unoccupied_count() as usize * natoms_per_residue;
43
+ let natoms_substitutes = substitutions.iter().map(|s| s.natoms()).sum::<usize>();
44
+ let natoms = natoms_structure + natoms_solvent + natoms_substitutes;
45
+
46
+ // Write the gro header.
47
+ writeln!(
48
+ writer,
49
+ "solvated by {} (v{})",
50
+ env!("CARGO_BIN_NAME"),
51
+ bentopy::core::version::VERSION
52
+ )?;
53
+ writeln!(writer, "{natoms}")?;
54
+
55
+ // First, we write the original structure.
56
+ let start = std::time::Instant::now();
57
+ eprint!("Writing original structure ({natoms_structure:>9}/{natoms} total atoms)... ");
58
+ for line in structure.format_atom_lines_iter() {
59
+ writer.write_all(line.as_bytes())?;
60
+ }
61
+ eprintln!("Done, took {:.3} s.", start.elapsed().as_secs_f32());
62
+
63
+ // Now for the solvent. We will go over the cells described by the placemap, and format and
64
+ // write the atom lines per cell. This means that we can avoid creating the whole solvent
65
+ // structure at once, which would be tremendously memory intensive.
66
+ let start = std::time::Instant::now();
67
+ eprintln!("Writing solvent structure ({natoms_solvent:>9}/{natoms} total atoms)...");
68
+ let mut placements = solvent_placemap.iter_atoms_chunks();
69
+ let mut particle_buffer = Vec::new();
70
+ let mut n = 0;
71
+ 'write_atoms: loop {
72
+ particle_buffer.clear();
73
+
74
+ // TODO: I think it's scary that buffer_size could feasibly be zero.
75
+ 'fill_buffer: while particle_buffer.len() < buffer_size {
76
+ match placements.next() {
77
+ Some(sp) => particle_buffer.extend(sp),
78
+ None if particle_buffer.is_empty() => break 'write_atoms,
79
+ None => break 'fill_buffer,
80
+ }
81
+ }
82
+
83
+ if PAR {
84
+ let lines: String = particle_buffer
85
+ .par_iter()
86
+ .map(Structure::format_atom_line)
87
+ .collect();
88
+ writer.write_all(lines.as_bytes())?;
89
+ } else {
90
+ // TODO: Even this could be done with a buffer of lines that fills up for a while,
91
+ // and then we write it. So non-parallel but still buffered. But in a way that is
92
+ // equivalent to making sure we use a BufWriter. Maybe first benchmark if requiring a
93
+ // BufWriter in the function signature (we already use it at the call site) makes a
94
+ // difference (it shouldn't because the impl Write makes it generic over our
95
+ // BufWriter). Only then try to see if rolling our own helps out.
96
+ for atom in &particle_buffer {
97
+ let line = Structure::format_atom_line(atom);
98
+ writer.write_all(line.as_bytes())?;
99
+ }
100
+ }
101
+
102
+ // Report the progress.
103
+ n += particle_buffer.len();
104
+ let progress = n as f32 / natoms_solvent as f32;
105
+ let percentage = progress * 100.0;
106
+ let delta = std::time::Instant::now() - start;
107
+ let time_left = delta.as_secs_f32() * (progress.recip() - 1.0);
108
+ eprint!(
109
+ "\r({percentage:>4.1}%) Wrote {n:>9}/{natoms_solvent} solvent atoms. (ETA {time_left:>4.0} s) "
110
+ );
111
+ }
112
+ eprintln!();
113
+ eprintln!(
114
+ "Done writing solvent atoms, took {:.3} s.",
115
+ start.elapsed().as_secs_f32()
116
+ );
117
+
118
+ if !substitutions.is_empty() {
119
+ // Write out the substitutes at the end of the structure.
120
+ let start = std::time::Instant::now();
121
+ eprintln!("Writing substitutes ({natoms_substitutes:>9}/{natoms} total atoms)...");
122
+ for substitution in substitutions {
123
+ let name = substitution.name();
124
+ let number = substitution.natoms();
125
+ eprint!("\t{name}, {number} atoms... ");
126
+ let start = std::time::Instant::now();
127
+ let mut placements = substitution.iter_atoms();
128
+ loop {
129
+ particle_buffer.clear();
130
+ particle_buffer.extend(placements.by_ref().take(buffer_size));
131
+ if particle_buffer.is_empty() {
132
+ break;
133
+ }
134
+
135
+ let beads = &particle_buffer;
136
+ if PAR {
137
+ let lines: String = beads.par_iter().map(Structure::format_atom_line).collect();
138
+ writer.write_all(lines.as_bytes())?;
139
+ } else {
140
+ for atom in beads {
141
+ let line = Structure::format_atom_line(atom);
142
+ writer.write_all(line.as_bytes())?;
143
+ }
144
+ }
145
+ }
146
+ eprintln!("Took {:.3} s.", start.elapsed().as_secs_f32());
147
+ }
148
+ eprintln!(
149
+ "Done writing substitutes, took {:.3} s.",
150
+ start.elapsed().as_secs_f32()
151
+ );
152
+ }
153
+
154
+ // Finally, we write the box vectors.
155
+ writeln!(writer, "{}", structure.boxvecs())?;
156
+
157
+ writer.flush()?;
158
+
159
+ Ok(())
160
+ }
solvate/substitute.rs ADDED
@@ -0,0 +1,113 @@
1
+ use eightyseven::structure::{Atom, AtomName};
2
+
3
+ use crate::{args::Substitute, placement::PlaceMap};
4
+
5
+ pub struct Substitution<'sol> {
6
+ name: AtomName,
7
+ placemap: PlaceMap<'sol>,
8
+ }
9
+
10
+ impl<'sol> Substitution<'sol> {
11
+ pub fn new(name: impl Into<AtomName>, placemap: PlaceMap<'sol>) -> Self {
12
+ Self {
13
+ name: name.into(),
14
+ placemap,
15
+ }
16
+ }
17
+
18
+ pub fn natoms(&self) -> usize {
19
+ self.placemap.unoccupied_count() as usize
20
+ }
21
+
22
+ pub fn name(&self) -> &str {
23
+ &self.name
24
+ }
25
+
26
+ /// Iterate over all atoms in this [`Substitution`].
27
+ pub fn iter_atoms(&self) -> impl Iterator<Item = Atom> + '_ {
28
+ self.placemap.iter_positions().enumerate().map(|(i, pos)| {
29
+ let num = i as u32 + 1;
30
+ Atom {
31
+ resnum: num,
32
+ resname: self.name,
33
+ atomname: self.name,
34
+ atomnum: num,
35
+ position: pos,
36
+ velocity: Default::default(),
37
+ }
38
+ })
39
+ }
40
+
41
+ /// Fuse the contents of two [`Substitution`] maps.
42
+ pub fn glue(&mut self, substitution: &Substitution<'sol>) {
43
+ self.placemap &= substitution.placemap.clone();
44
+ }
45
+ }
46
+
47
+ pub fn substitute<'sol>(
48
+ rng: &mut rand::rngs::StdRng,
49
+ placemap: &mut PlaceMap<'sol>,
50
+ substitutes: &[Substitute],
51
+ ) -> Vec<Substitution<'sol>> {
52
+ use rand::seq::SliceRandom;
53
+
54
+ // FIXME: Make this into a nicer error. Already explains the situation well, but the assert
55
+ // failure is ugly in the output.
56
+ let n_free = placemap.unoccupied_count();
57
+ let n_subs = substitutes.iter().map(|s| s.number).sum::<u64>();
58
+ assert!(
59
+ n_subs <= n_free,
60
+ "the number of substitutions that are specified ({n_subs}) exceeds the number of \
61
+ solvent positions that can be substituted ({n_free})"
62
+ );
63
+
64
+ let mut substitutions = Vec::new();
65
+ let mut zebra = Vec::new();
66
+ for substitute in substitutes {
67
+ eprint!(
68
+ "\tSubstituting {} solvent locations with {}... ",
69
+ substitute.number, substitute.name
70
+ );
71
+ let start = std::time::Instant::now();
72
+ if substitute.number == 0 {
73
+ eprintln!("Skipping.");
74
+ continue;
75
+ }
76
+
77
+ let n_free = placemap.unoccupied_count();
78
+ if n_free == 0 {
79
+ eprintln!("No free solvent spots left.");
80
+ break;
81
+ }
82
+
83
+ zebra.clear();
84
+ zebra.resize(n_free as usize, false);
85
+ zebra[..substitute.number as usize].fill(true);
86
+ zebra.shuffle(rng);
87
+ let mut zebra = zebra.iter();
88
+
89
+ let mut old_placemap = placemap.clone(); // Becomes the map for our substitute.
90
+ for mut placement in placemap.iter_placements_mut() {
91
+ for i in 0..placement.len() {
92
+ let is_occupied = placement.get(i);
93
+ // If this position is not already occupied by a non-solvent bead, and if our
94
+ // shuffled values indicate that we need to place a substitute here, we set this
95
+ // bead to occupied in the solvent placemap.
96
+ if !is_occupied && *zebra.next().unwrap() {
97
+ placement.set_occupied(i)
98
+ }
99
+ }
100
+ }
101
+ assert_eq!(zebra.len(), 0);
102
+
103
+ old_placemap ^= placemap.clone();
104
+ let substitute_placemap = !old_placemap;
105
+ substitutions.push(Substitution::new(
106
+ substitute.name.as_str(),
107
+ substitute_placemap,
108
+ ));
109
+ eprintln!("Took {:.3} s.", start.elapsed().as_secs_f32());
110
+ }
111
+
112
+ substitutions
113
+ }