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
mask/utilities.py ADDED
@@ -0,0 +1,38 @@
1
+ from pathlib import Path
2
+
3
+ import MDAnalysis as mda
4
+ import numpy as np
5
+
6
+
7
+ def voxels_to_gro(path: Path, arr, scale: float = 1.0, place_in_center: bool = True):
8
+ scale *= 10.0 # We go from Å to nm scale.
9
+ n_atoms = np.prod(arr.shape)
10
+
11
+ # Set up our output universe.
12
+ u = mda.Universe.empty(
13
+ n_atoms,
14
+ n_residues=n_atoms,
15
+ atom_resindex=range(n_atoms),
16
+ residue_segindex=[0] * n_atoms,
17
+ trajectory=True,
18
+ )
19
+
20
+ # Fill it up with an 3×n_atoms coordinate array.
21
+ indices = np.array(list(np.ndindex(arr.shape)))
22
+ u.atoms.positions = indices * scale
23
+
24
+ if place_in_center:
25
+ # Place the atoms in the _middle_ of their voxel, rather than at the voxel origin.
26
+ u.atoms.positions += scale / 2
27
+
28
+ # Derive the box size from the array shape.
29
+ box = np.array(arr.shape) * scale
30
+ u.dimensions = np.array([*box, 90, 90, 90], dtype=np.int32)
31
+
32
+ # Assign appropriate residue names. We want to distinguish between the
33
+ # different values we find in the array (the different compartments!).
34
+ names = [str(name) for name in arr.flatten()]
35
+ u.add_TopologyAttr("name", names)
36
+
37
+ # Write them!
38
+ u.atoms.write(path)
merge/merge.py ADDED
@@ -0,0 +1,175 @@
1
+ import argparse
2
+ from sys import stderr
3
+
4
+ # This should be sufficient space to write the final natoms into.
5
+ NATOMS_PLACEHOLDER = " " * 32
6
+ RESNAME_MAX_LEN = 5
7
+
8
+
9
+ class InputFile:
10
+ divider = ":"
11
+
12
+ def __init__(self, s: str):
13
+ if self.divider in s:
14
+ path, resname = s.split(self.divider)
15
+ if len(resname) > RESNAME_MAX_LEN:
16
+ raise ValueError(
17
+ f"A resname cannot be longer than 5 characters, found '{resname}' with {len(resname)} characters"
18
+ )
19
+ else:
20
+ path = s
21
+ resname = None
22
+ self.file = argparse.FileType("r")(path)
23
+ self.resname = resname
24
+
25
+
26
+ def eprint(*args, **kwargs):
27
+ """
28
+ Print to standard error.
29
+ """
30
+ print(*args, **kwargs, file=stderr)
31
+
32
+
33
+ def guarantee_newline(line: str) -> str:
34
+ """
35
+ If the provided line does not have a trailing newline, yet, add it.
36
+ Otherwise leave it alone.
37
+
38
+ Note that this is an idempotent operation (i.e., ∀x f(x) = f(f(x))).
39
+ """
40
+ if line.endswith("\n"):
41
+ return line
42
+ else:
43
+ return line + "\n"
44
+
45
+
46
+ def parse_boxvec(s: str) -> tuple[float]:
47
+ """
48
+ Parse a gro box vector line.
49
+ """
50
+ boxvec = [float(v) for v in s.split()]
51
+ nv = len(boxvec)
52
+ if nv == 3 or nv == 9:
53
+ return boxvec
54
+ raise ValueError(
55
+ f"Could not parse '{s}'. The box vector must have either 3 or 9 values, found {nv}."
56
+ )
57
+
58
+
59
+ def merge(args):
60
+ output = args.output
61
+ if not output.seekable():
62
+ eprint("ERROR: Output into non-seekable files is currently not supported.")
63
+ return 1
64
+
65
+ # Write the title.
66
+ if args.title is not None:
67
+ output.write(guarantee_newline(args.title))
68
+
69
+ boxvecs = []
70
+ natoms_total = 0
71
+ natoms_location = None # The location in output where the natoms are written.
72
+ # We know that there will be at least one file provided through args.
73
+ for input_file in args.files:
74
+ # TODO: Unpack this with a tuple in the for loop? How can we implement that for a custom class?
75
+ file = input_file.file
76
+ resname = input_file.resname
77
+
78
+ # Report on the file and if we replace the resnames.
79
+ if resname is None:
80
+ info = "Leaving resnames in place."
81
+ else:
82
+ assert (
83
+ len(resname) <= RESNAME_MAX_LEN
84
+ ) # Already verified during argument parsing.
85
+ info = f"Replacing resnames with '{resname}'."
86
+ eprint(f"Reading from {file.name}...", info)
87
+
88
+ next(file) # Skip the title.
89
+
90
+ # Put in a natoms placeholder if this is the first file that is written.
91
+ if natoms_location is None:
92
+ natoms_location = output.tell()
93
+ output.write(NATOMS_PLACEHOLDER + "\n")
94
+
95
+ # Read the number of atoms that are in this file.
96
+ natoms = int(next(file).strip())
97
+
98
+ # Write that number of lines into the output file.
99
+ if resname is not None:
100
+ # Modify the resname for each atom.
101
+ padded_resname = f"{resname:<5}"
102
+ for _ in range(natoms):
103
+ line = next(file)
104
+ output.write(line[:5])
105
+ output.write(padded_resname)
106
+ output.write(line[10:])
107
+ else:
108
+ for _ in range(natoms):
109
+ output.write(next(file))
110
+
111
+ # Now that we have written the lines, we'll add them to our total.
112
+ natoms_total += natoms
113
+
114
+ # Parse the box vector and add it to our collection.
115
+ boxvec = parse_boxvec(next(file))
116
+ boxvecs.append(boxvec)
117
+
118
+ # TODO: Take into account args.box.
119
+ # Write out the final box vector.
120
+ final_boxvec = boxvecs[0]
121
+ output.write(" ".join([str(v) for v in final_boxvec]) + "\n")
122
+
123
+ # Finally, we go and seek back to the start, where we left our natoms placeholder.
124
+ # Let's replace that with the correct total number of atom records we wrote to output.
125
+ output.seek(natoms_location)
126
+ output.write(str(natoms_total))
127
+
128
+
129
+ def main():
130
+ parser = argparse.ArgumentParser(
131
+ description="Concatenate gro files.",
132
+ )
133
+
134
+ parser.add_argument(
135
+ "files",
136
+ type=InputFile,
137
+ nargs="+",
138
+ help="""Files to concatenate (gro; <path>[:<resname>]).
139
+
140
+ Optionally, a residue name can be set for all atoms in a file by
141
+ appending a colon followed by the residue name.
142
+ Note that this name can be at most 5 characters long.
143
+
144
+ Replacing the residue names can be very useful in distinguishing between
145
+ parts of very large systems within a concatenated file.""",
146
+ )
147
+ parser.add_argument(
148
+ "-o",
149
+ "--output",
150
+ type=argparse.FileType("w"),
151
+ required=True,
152
+ help="Output path.",
153
+ )
154
+ parser.add_argument(
155
+ "-t",
156
+ "--title",
157
+ type=str,
158
+ default="bentopy merge",
159
+ help="Set the final title. (default: %(default)s)",
160
+ )
161
+ parser.add_argument(
162
+ "-b",
163
+ "--box",
164
+ type=parse_boxvec,
165
+ help="""Set the final box vectors.
166
+ Expects a valid gro box line, which is a space-separated list of either 3 or 9 floats.
167
+ By default, the box vector of the first file is chosen.""",
168
+ )
169
+
170
+ args = parser.parse_args()
171
+ merge(args)
172
+
173
+
174
+ if __name__ == "__main__":
175
+ main()
pack/args.rs ADDED
@@ -0,0 +1,77 @@
1
+ use std::path::PathBuf;
2
+
3
+ use clap::{Parser, ValueEnum};
4
+
5
+ /// Pack a space.
6
+ #[derive(Debug, Parser)]
7
+ #[command(version = bentopy::core::version::VERSION)]
8
+ pub struct Args {
9
+ // Configuration input file to define the run (bent).
10
+ pub config: PathBuf,
11
+
12
+ // Placement list output file (json).
13
+ pub output: PathBuf,
14
+
15
+ /// Sort the input structures by approximate size to optimize packing.
16
+ ///
17
+ /// Different heuristics are available.
18
+ #[arg(long, default_value = "moment")]
19
+ pub rearrange: RearrangeMethod,
20
+
21
+ /// Random number generator seed.
22
+ #[arg(long)]
23
+ pub seed: Option<u64>,
24
+
25
+ /// Set the bead radius that is considered during voxelization in nm.
26
+ ///
27
+ /// If set, this value overwrites any value provided in the input file.
28
+ /// If this value is not provided in the input file, the default value is 0.20 nm.
29
+ #[arg(long)]
30
+ pub bead_radius: Option<f64>,
31
+
32
+ /// Set the multiplier for determining the maximum number of tries for placing a segment.
33
+ ///
34
+ /// The number of segments requested is multiplied with this value to set the upper limit on
35
+ /// how many unsuccessful attempts will be made to place it.
36
+ ///
37
+ /// Setting a higher value will result in more attempts, which can help improve the total
38
+ /// number of segments packed in very dense systems.
39
+ ///
40
+ /// If set, this value overwrites any value provided in the input file.
41
+ /// If this value is not provided in the input file, the default value is 1000.
42
+ #[arg(long)]
43
+ pub max_tries_mult: Option<u64>,
44
+
45
+ /// Set the divisor for determining the maximum number of tries for placing a segment per
46
+ /// rotation.
47
+ ///
48
+ /// This is a rather advanced option, and it is unlikely that it requires tweaking.
49
+ ///
50
+ /// The maximum number of attempts (see `--max-tries-mult`) is divided by this divisor value to
51
+ /// determine the number of unsuccessful attempts that are allowed for a given rotation. After
52
+ /// this number of unsuccessful tries, the next random rotation will be tried.
53
+ ///
54
+ /// If set, this value overwrites any value provided in the input file.
55
+ /// If this value is not provided in the input file, the default value is 100.
56
+ #[arg(long)]
57
+ pub max_tries_rot_div: Option<u64>,
58
+
59
+ /// Disable printing the summary after the placement procedure.
60
+ #[arg(long)]
61
+ pub no_summary: bool,
62
+
63
+ /// Display verbose output.
64
+ #[arg(short, long)]
65
+ pub verbose: bool,
66
+ }
67
+
68
+ #[derive(Debug, Clone, ValueEnum)]
69
+ pub enum RearrangeMethod {
70
+ /// Use the geometric moment (sum of squared distances from geometric center).
71
+ #[value(alias = "moment-of-inertia")]
72
+ Moment,
73
+ Volume,
74
+ BoundingSphere,
75
+ /// Keep the arrangement specified in the input file.
76
+ None,
77
+ }
pack/main.rs ADDED
@@ -0,0 +1,121 @@
1
+ use std::io::{self, Read};
2
+
3
+ use anyhow::{Context, bail};
4
+ use args::Args;
5
+ use ariadne::{Color, Fmt};
6
+ use clap::Parser;
7
+
8
+ use bentopy::core::config::{Config, bent};
9
+ use bentopy::core::utilities::CLEAR_LINE;
10
+ use state::State;
11
+
12
+ mod args;
13
+ mod mask;
14
+ mod session;
15
+ mod state;
16
+ mod structure;
17
+ mod voxelize;
18
+
19
+ type Location = usize;
20
+
21
+ struct Summary {
22
+ entries: Vec<(String, usize, usize, f64)>,
23
+ }
24
+
25
+ impl Summary {
26
+ fn new() -> Self {
27
+ Self {
28
+ entries: Vec::new(),
29
+ }
30
+ }
31
+
32
+ fn push(&mut self, name: String, target: usize, placed: usize, time: f64) {
33
+ self.entries.push((name, target, placed, time))
34
+ }
35
+
36
+ fn present(&self, packing_duration: f64) {
37
+ fn percentage(hits: usize, target: usize) -> f32 {
38
+ match target {
39
+ 0 => 0.0,
40
+ _ => (hits as f32 / target as f32) * 100.0,
41
+ }
42
+ }
43
+
44
+ println!("idx \tname \t ok%\ttarget\tplaced\ttime (s)\tremark");
45
+ println!("----\t----------\t------\t------\t------\t--------\t------");
46
+ let mut target_tot = 0;
47
+ let mut hits_tot = 0;
48
+ for (i, (name, target, hits, duration)) in self.entries.iter().enumerate() {
49
+ let perc = percentage(*hits, *target);
50
+ let ok = if hits == target { " " } else { "<" };
51
+ println!(
52
+ "{i:>4}\t{name:<10}\t{perc:>5.1}%\t{target:>6}\t{hits:>6}\t{duration:8.2}\t{ok}"
53
+ );
54
+ target_tot += target;
55
+ hits_tot += hits;
56
+ }
57
+ let perc = percentage(hits_tot, target_tot);
58
+ let ok = if hits_tot == target_tot { " " } else { "<" };
59
+ println!(
60
+ " \t \t{perc:>5.1}%\t{target_tot:>6}\t{hits_tot:>6}\t{packing_duration:8.2}\t{ok}"
61
+ )
62
+ }
63
+ }
64
+
65
+ fn main() -> anyhow::Result<()> {
66
+ // Preparation.
67
+ let args = Args::parse();
68
+ let mut state = {
69
+ let config_path = &args.config;
70
+ if config_path.extension().is_some_and(|ext| ext == "json") {
71
+ eprintln!("NOTE: Legacy bentopy input files can be converted to .bent files:");
72
+ let old_path = config_path.to_str().unwrap().fg(Color::Yellow);
73
+ let bent_path = config_path.with_extension("bent");
74
+ let bent_path = bent_path.to_str().unwrap().fg(Color::Cyan);
75
+ eprintln!("You can convert {old_path} to a bent file using the following command:");
76
+ eprintln!();
77
+ eprintln!("\tbentopy-init convert -i {old_path} -o {bent_path}");
78
+ eprintln!();
79
+ bail!("Legacy .json bentopy input files are deprecated")
80
+ }
81
+ let mut file = std::fs::File::open(config_path)
82
+ .with_context(|| format!("Failed to open the configuration file {config_path:?}"))?;
83
+ let mut buf = String::new();
84
+ file.read_to_string(&mut buf)?;
85
+ let config: Config = bent::parse_config(config_path.to_str().unwrap(), &buf)
86
+ .with_context(|| format!("Failed to process configuration from {config_path:?}"))?;
87
+
88
+ State::new(args, config).context("Failed to set up program state")?
89
+ };
90
+
91
+ // Check whether the compartment masks make any sense.
92
+ state
93
+ .check_masks()
94
+ .context("Encountered a problem while checking the compartment masks")?;
95
+
96
+ // Packing.
97
+ let packing_start = std::time::Instant::now();
98
+ let (placements, summary) = state
99
+ .pack(&mut io::stderr())
100
+ .context("Encountered a problem while packing")?;
101
+ let packing_duration = packing_start.elapsed().as_secs_f64();
102
+ eprintln!("Packing process took {packing_duration:.3} s.");
103
+
104
+ // Final summary.
105
+ if state.summary {
106
+ summary.present(packing_duration);
107
+ }
108
+
109
+ // Output.
110
+ let placement_list_path = &state.output.path;
111
+ eprint!("Writing placement list to {placement_list_path:?}... ");
112
+ let start_output = std::time::Instant::now();
113
+ let placement_list_file = std::fs::File::create(placement_list_path)
114
+ .with_context(|| format!("Failed to create placement list file {placement_list_path:?}"))?;
115
+ let placement_list = state.placement_list(placements);
116
+ serde_json::to_writer(placement_list_file, &placement_list)
117
+ .context("Encountered a problem while writing the placement list")?;
118
+ eprintln!("Done in {:.3} s.", start_output.elapsed().as_secs_f64());
119
+
120
+ Ok(())
121
+ }