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/structure.rs ADDED
@@ -0,0 +1,185 @@
1
+ use std::io::{BufRead, BufReader};
2
+ use std::num::NonZeroUsize;
3
+
4
+ use eightyseven::reader::{ParseList, ReadGro};
5
+ use eightyseven::structure::{AtomName, AtomNum, ResName, ResNum};
6
+ use eightyseven::writer::{WriteGro, format_atom_line};
7
+ use glam::Vec3;
8
+
9
+ pub type Atom = Vec3;
10
+
11
+ /// Load a [`Structure`] from a structure file.
12
+ ///
13
+ /// This function will return an error if the structure is empty. Downstream functions may assume
14
+ /// that a [`Structure`] has at least one atom.
15
+ pub fn load_molecule<P: AsRef<std::path::Path> + std::fmt::Debug>(
16
+ path: P,
17
+ ) -> anyhow::Result<Structure> {
18
+ use anyhow::{Context, bail};
19
+
20
+ let file = std::fs::File::open(&path)?;
21
+
22
+ let structure = match path.as_ref().extension().and_then(|s| s.to_str()) {
23
+ Some("gro") => Structure::read_from_file(file)
24
+ .with_context(|| format!("Failed to parse gro file {path:?}"))?,
25
+ Some("pdb") => Structure::read_from_pdb_file(file)
26
+ .with_context(|| format!("Failed to parse PDB file {path:?}"))?,
27
+ None | Some(_) => {
28
+ eprintln!("WARNING: Assuming {path:?} is a PDB file.");
29
+ Structure::read_from_pdb_file(file)
30
+ .with_context(|| format!("Failed to parse the file {path:?} as PDB"))?
31
+ }
32
+ };
33
+
34
+ if structure.natoms() == 0 {
35
+ bail!("Structure from {path:?} contains no atoms")
36
+ }
37
+
38
+ Ok(structure)
39
+ }
40
+
41
+ /// A structure type that stores its atoms as simple positions.
42
+ ///
43
+ /// Invariant: A `Structure` is always centered, such that its geometric center lies at the origin.
44
+ ///
45
+ /// Invariant: A `Structure` has at least one atom.
46
+ pub struct Structure {
47
+ atoms: Vec<Atom>,
48
+ }
49
+
50
+ impl ReadGro<Atom> for Structure {
51
+ const PARSE_LIST: ParseList = ParseList {
52
+ resnum: false,
53
+ resname: false,
54
+ atomname: false,
55
+ atomnum: false,
56
+ position: true,
57
+ velocity: false,
58
+ };
59
+
60
+ fn build_atom(
61
+ _resnum: Option<ResNum>,
62
+ _resname: Option<ResName>,
63
+ _atomname: Option<AtomName>,
64
+ _atomnum: Option<AtomNum>,
65
+ position: Option<[f32; 3]>,
66
+ _velocity: Option<[f32; 3]>,
67
+ ) -> Atom {
68
+ // We can safely unwrap because this is the only value we expect.
69
+ Atom::from_array(position.unwrap())
70
+ }
71
+
72
+ fn build_structure(
73
+ _title: String,
74
+ atoms: Vec<Atom>,
75
+ _boxvecs: eightyseven::structure::BoxVecs,
76
+ ) -> Self {
77
+ let mut structure = Self { atoms };
78
+ structure.translate_to_center();
79
+ structure
80
+ }
81
+ }
82
+
83
+ #[non_exhaustive]
84
+ #[derive(Debug)]
85
+ pub enum ParsePdbError {
86
+ IOError(std::io::Error),
87
+ BadPositionValue {
88
+ err: std::num::ParseFloatError,
89
+ /// Line number.
90
+ ln: NonZeroUsize,
91
+ },
92
+ }
93
+
94
+ impl std::error::Error for ParsePdbError {}
95
+
96
+ impl std::fmt::Display for ParsePdbError {
97
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98
+ match self {
99
+ Self::IOError(err) => write!(f, "IO error: {err}"),
100
+ Self::BadPositionValue { err, ln } => {
101
+ write!(f, "Could not parse record on line {ln}: {err}")
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ impl From<std::io::Error> for ParsePdbError {
108
+ fn from(err: std::io::Error) -> Self {
109
+ Self::IOError(err)
110
+ }
111
+ }
112
+
113
+ impl Structure {
114
+ pub fn read_from_pdb_file(file: std::fs::File) -> Result<Structure, ParsePdbError> {
115
+ let reader = BufReader::new(file);
116
+
117
+ let mut atoms = Vec::new();
118
+ for (ln, line) in reader.lines().enumerate() {
119
+ let line = line?;
120
+ if line.starts_with("ATOM") || line.starts_with("HETATM") {
121
+ let ln = NonZeroUsize::new(ln + 1).unwrap(); // We know ln + 1 >= 1 because ln >= 0.
122
+ let bad_position = |err| ParsePdbError::BadPositionValue { err, ln };
123
+ let x = line[30..38].trim().parse().map_err(bad_position)?;
124
+ let y = line[38..46].trim().parse().map_err(bad_position)?;
125
+ let z = line[46..54].trim().parse().map_err(bad_position)?;
126
+ let atom = Atom::new(x, y, z) / 10.0; // Convert from Å to nm.
127
+ atoms.push(atom);
128
+ }
129
+ }
130
+
131
+ let mut structure = Self { atoms };
132
+ structure.translate_to_center();
133
+ Ok(structure)
134
+ }
135
+
136
+ /// Translate this [`Structure`] such that it's geometric center lies at the origin.
137
+ fn translate_to_center(&mut self) {
138
+ // Invariant: A Structure has at least one atom.
139
+ let center = self.atoms().sum::<Atom>() / self.natoms() as f32;
140
+ for pos in &mut self.atoms {
141
+ *pos -= center;
142
+ }
143
+ }
144
+
145
+ /// Calculate the moment of inertia for this [`Structure`].
146
+ ///
147
+ /// Invariant: Assumes that the structure is centered.
148
+ ///
149
+ /// I = Σ(m_i * r_i²)
150
+ pub fn moment_of_inertia(&self) -> f32 {
151
+ self.atoms().map(|atom| atom.length_squared()).sum()
152
+ }
153
+
154
+ /// Returns the radius of the point that is farthest from the structure geometric center.
155
+ ///
156
+ /// Invariant: Assumes that the structure is centered.
157
+ pub fn bounding_sphere(&self) -> f32 {
158
+ self.atoms()
159
+ .map(|atom| atom.length())
160
+ .max_by(f32::total_cmp)
161
+ .unwrap() // Invariant: A structure has at least one atom.
162
+ }
163
+ }
164
+
165
+ impl<'atoms> WriteGro<'atoms, Atom> for Structure {
166
+ fn title(&self) -> String {
167
+ "debug".to_string()
168
+ }
169
+
170
+ fn natoms(&self) -> usize {
171
+ self.atoms.len()
172
+ }
173
+
174
+ fn atoms(&'atoms self) -> impl Iterator<Item = &'atoms Atom> {
175
+ self.atoms.iter()
176
+ }
177
+
178
+ fn boxvecs(&self) -> String {
179
+ "400.0 400.0 400.0".to_string()
180
+ }
181
+
182
+ fn format_atom_line(atom: &Atom) -> String {
183
+ format_atom_line(1, "DUMMY", "DUMMY", 2, atom.to_array(), None)
184
+ }
185
+ }
pack/voxelize.rs ADDED
@@ -0,0 +1,85 @@
1
+ use eightyseven::writer::WriteGro;
2
+ use glam::{U64Vec3, Vec3A};
3
+
4
+ use crate::state::{Rotation, Voxels};
5
+ use crate::structure::Structure;
6
+
7
+ /// Returns whether a sphere intersects with an axis-aligned, unit-sized cube.
8
+ #[inline(always)]
9
+ fn overlap(cube_center: Vec3A, sphere_center: Vec3A, sphere_radius_squared: f32) -> bool {
10
+ let c = 0.5; // Half side-length of the cube.
11
+ let rsc = sphere_center - cube_center;
12
+ let l = Vec3A::max(rsc.abs() - c, Vec3A::ZERO);
13
+ let l_squared = l * l;
14
+ l_squared.element_sum() <= sphere_radius_squared
15
+ }
16
+
17
+ /// Conservatively voxelize a [`Structure`].
18
+ ///
19
+ /// Invariant: A [`Structure`] has at least one atom.
20
+ pub fn voxelize(structure: &Structure, rotation: Rotation, resolution: f32, radius: f32) -> Voxels {
21
+ // Extract and rotate the points from the structure.
22
+ // And make them aligned (Vec3A) to encourage vectorization as well.
23
+ let mut points: Box<[Vec3A]> = structure
24
+ .atoms()
25
+ .map(|&position| rotation.mul_vec3a(position.into()))
26
+ .collect();
27
+
28
+ // Invariant: A Structure has at least one atom.
29
+ let npoints = points.len();
30
+ assert!(npoints >= 1, "a Structure has at least one atom");
31
+
32
+ // Invariant: A Structure has at least one atom.
33
+ let min = points.iter().copied().reduce(|a, b| a.min(b)).unwrap();
34
+ // Subtract one bead radius such that the lower bounds are treated correctly.
35
+ let min = min - radius;
36
+ // Translate the points such that they are all above (0, 0, 0).
37
+ points.iter_mut().for_each(|p| *p -= min);
38
+ // Find the bounds of our voxel grid.
39
+ // Invariant: A Structure has at least one atom.
40
+ let max = points.iter().copied().reduce(|a, b| a.max(b)).unwrap();
41
+ let max = max + radius;
42
+
43
+ // Set up our voxels.
44
+ let dimensions = (max / resolution).ceil().as_u64vec3();
45
+ let mut voxels = Voxels::new(dimensions.to_array());
46
+
47
+ // Transform points according to the resolution.
48
+ let scaled_points = {
49
+ points.iter_mut().for_each(|p| *p /= resolution);
50
+ points
51
+ };
52
+ // Scale the radius for the resolution along with the points.
53
+ let scaled_radius = radius / resolution;
54
+ let scaled_radius_squared = scaled_radius.powi(2);
55
+
56
+ // Go through each point and set the voxels its sphere radius occupies.
57
+ for point in scaled_points {
58
+ let lower_bound = point - scaled_radius;
59
+ let upper_bound = point + scaled_radius;
60
+ debug_assert!(Vec3A::cmpgt(lower_bound + 1e-5, Vec3A::ZERO).all());
61
+ debug_assert!(Vec3A::cmplt(upper_bound - 1e-5, max / resolution).all());
62
+ let lower_bound = lower_bound.floor().as_u64vec3();
63
+ let upper_bound = upper_bound.ceil().as_u64vec3();
64
+
65
+ for z in lower_bound.z..upper_bound.z {
66
+ for y in lower_bound.y..upper_bound.y {
67
+ for x in lower_bound.x..upper_bound.x {
68
+ let position = U64Vec3::new(x, y, z);
69
+ let cube_center = Vec3A::from(position.as_vec3() + 0.5);
70
+ if overlap(cube_center, point, scaled_radius_squared) {
71
+ let idx = position.to_array();
72
+ voxels.set(idx, true);
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ // Invariant: A Structure has at least one atom.
80
+ debug_assert!(
81
+ voxels.any::<true>(),
82
+ "the produced voxel mask contains no occupied voxels, should be impossible"
83
+ );
84
+ voxels
85
+ }
render/args.rs ADDED
@@ -0,0 +1,109 @@
1
+ use std::{path::PathBuf, str::FromStr};
2
+
3
+ use crate::limits::Limits;
4
+ use clap::{Parser, ValueEnum};
5
+
6
+ /// Render structures from a placement list into a gro file.
7
+ ///
8
+ ///
9
+ /// Structures specified in the placement list are retrieved from their pdb or gro
10
+ /// files and placed into a gro file according to their rotations and positions.
11
+ #[derive(Debug, Parser)]
12
+ #[command(about, version = bentopy::core::version::VERSION)]
13
+ pub struct Args {
14
+ /// Path to the placement list.
15
+ ///
16
+ /// To read from stdin, pass "-".
17
+ pub input: PathBuf,
18
+
19
+ /// Output gro file path.
20
+ pub output: PathBuf,
21
+
22
+ /// Write a topology (.top) file.
23
+ #[arg(short, long)]
24
+ pub topol: Option<PathBuf>,
25
+
26
+ /// Root path for the structure paths.
27
+ ///
28
+ /// When set, this path will be prepended to any relative path pointing to a structure in the
29
+ /// placement list. Absolute paths are respected.
30
+ #[arg(long)]
31
+ pub root: Option<PathBuf>,
32
+
33
+ /// Granularity of the produced output.
34
+ #[arg(long, value_enum, default_value_t = Mode::Full, conflicts_with="topol")]
35
+ pub mode: Mode,
36
+
37
+ /// Write out a unique resnum for each segment instance, or use one grouped resnum for each
38
+ /// instance of a segment.
39
+ #[arg(long, value_enum, default_value_t)]
40
+ pub resnum_mode: ResnumMode,
41
+
42
+ /// Only render structures that have a position within a smaller cuboid.
43
+ ///
44
+ /// Arguments can be provided as a comma-separated array of 6 values. Each value can be a
45
+ /// number indicating a bound or a non-numerical value indicating an unset bound.
46
+ #[arg(long)]
47
+ pub limits: Option<Limits>,
48
+
49
+ /// Retain the residue names from the input structure.
50
+ #[arg(long)]
51
+ pub ignore_tags: bool,
52
+
53
+ /// Print additional information.
54
+ #[arg(short, long)]
55
+ pub verbose: bool,
56
+ }
57
+
58
+ #[derive(Debug, Default, Clone, ValueEnum)]
59
+ pub enum Mode {
60
+ /// Output all atoms.
61
+ #[default]
62
+ Full,
63
+ /// Only the backbone atoms.
64
+ Backbone,
65
+ /// Only the alpha carbons.
66
+ Alpha,
67
+ /// One bead per residue.
68
+ Residue,
69
+ /// One bead per instance of a structure.
70
+ Instance,
71
+ }
72
+
73
+ impl FromStr for Mode {
74
+ type Err = ();
75
+
76
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
77
+ Ok(match s.to_lowercase().as_str() {
78
+ "full" => Self::Full,
79
+ "backbone" | "bb" => Self::Full,
80
+ "alpha" | "ca" | "a" => Self::Alpha,
81
+ "residue" | "res" => Self::Residue,
82
+ "instance" => Self::Instance,
83
+ _ => return Err(()),
84
+ })
85
+ }
86
+ }
87
+
88
+ #[derive(Debug, Default, Clone, ValueEnum)]
89
+ pub enum ResnumMode {
90
+ /// Each segment instance has a unique residue number.
91
+ #[default]
92
+ Instance,
93
+ /// All instances of a segment have the same residue number.
94
+ Segment,
95
+ /// The residue number appear exactly as in the input structure.
96
+ Keep,
97
+ }
98
+
99
+ impl FromStr for ResnumMode {
100
+ type Err = ();
101
+
102
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
103
+ Ok(match s.to_lowercase().as_str() {
104
+ "instance" => Self::Instance,
105
+ "segment" => Self::Segment,
106
+ _ => return Err(()),
107
+ })
108
+ }
109
+ }
render/limits.rs ADDED
@@ -0,0 +1,73 @@
1
+ use std::str::FromStr;
2
+
3
+ use glam::Vec3;
4
+
5
+ #[derive(Debug, Default, Clone)]
6
+ pub struct Limits {
7
+ pub minx: Option<f32>,
8
+ pub maxx: Option<f32>,
9
+ pub miny: Option<f32>,
10
+ pub maxy: Option<f32>,
11
+ pub minz: Option<f32>,
12
+ pub maxz: Option<f32>,
13
+ }
14
+
15
+ impl Limits {
16
+ pub fn is_inside(&self, point: impl Into<Vec3>) -> bool {
17
+ let point = point.into();
18
+ self.minx.is_none_or(|minx| minx <= point.x)
19
+ && self.miny.is_none_or(|miny| miny <= point.y)
20
+ && self.minz.is_none_or(|minz| minz <= point.z)
21
+ && self.maxx.is_none_or(|maxx| point.x <= maxx)
22
+ && self.maxy.is_none_or(|maxy| point.y <= maxy)
23
+ && self.maxz.is_none_or(|maxz| point.z <= maxz)
24
+ }
25
+
26
+ // NOTE: This implementation is slightly verbose, and could be shortened. But that is not
27
+ // necessary. This is not speed-critical and I'd rather keep it entirely clear what is
28
+ // going on.
29
+ /// Determine the appropriate box size based on the constraints posed by the [`Limits`].
30
+ pub fn box_size(&self, boxsize: Vec3) -> Vec3 {
31
+ // We bake the limits with the information from the original box size.
32
+ let baked_limits = Limits {
33
+ minx: Some(self.minx.unwrap_or(0.0)),
34
+ maxx: Some(self.maxx.unwrap_or(boxsize.x)),
35
+ miny: Some(self.miny.unwrap_or(0.0)),
36
+ maxy: Some(self.maxy.unwrap_or(boxsize.y)),
37
+ minz: Some(self.minz.unwrap_or(0.0)),
38
+ maxz: Some(self.maxz.unwrap_or(boxsize.z)),
39
+ };
40
+
41
+ // We can safely unwrap these, because we just initialized them as Some(f32).
42
+ Vec3::new(
43
+ baked_limits.maxx.unwrap() - baked_limits.minx.unwrap(),
44
+ baked_limits.maxy.unwrap() - baked_limits.miny.unwrap(),
45
+ baked_limits.maxz.unwrap() - baked_limits.minz.unwrap(),
46
+ )
47
+ }
48
+ }
49
+
50
+ impl FromStr for Limits {
51
+ type Err = String;
52
+
53
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
54
+ let Ok([minx, maxx, miny, maxy, minz, maxz]): Result<[Option<f32>; 6], _> = s
55
+ .split(',')
56
+ .map(|v| v.trim().parse().ok())
57
+ .collect::<Vec<_>>()
58
+ .try_into()
59
+ else {
60
+ let n = s.split(',').count();
61
+ return Err(format!("expected 6 values, found {n}"));
62
+ };
63
+
64
+ Ok(Self {
65
+ minx,
66
+ maxx,
67
+ miny,
68
+ maxy,
69
+ minz,
70
+ maxz,
71
+ })
72
+ }
73
+ }
render/main.rs ADDED
@@ -0,0 +1,12 @@
1
+ //! Render a placement list to a gro file with speed.
2
+ //!
3
+ //! By Marieke Westendorp, 2024.
4
+ //! <ma3ke.cyber@gmail.com>
5
+ mod args;
6
+ mod limits;
7
+ mod render;
8
+ mod structure;
9
+
10
+ fn main() -> anyhow::Result<()> {
11
+ render::render(clap::Parser::parse())
12
+ }