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
init/main.rs
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
use std::borrow::Cow;
|
|
2
|
+
use std::collections::HashSet;
|
|
3
|
+
use std::io::{BufWriter, Read, Write};
|
|
4
|
+
use std::path::PathBuf;
|
|
5
|
+
|
|
6
|
+
use anyhow::{Context, Result, bail};
|
|
7
|
+
use clap::{Parser, Subcommand};
|
|
8
|
+
|
|
9
|
+
use bentopy::core::config::{Config, Segment, legacy};
|
|
10
|
+
use bentopy::core::version::VERSION;
|
|
11
|
+
|
|
12
|
+
const BIN_NAME: &str = env!("CARGO_BIN_NAME");
|
|
13
|
+
|
|
14
|
+
#[derive(Debug, Parser)]
|
|
15
|
+
#[command(about, version = VERSION)]
|
|
16
|
+
struct Args {
|
|
17
|
+
#[command(subcommand)]
|
|
18
|
+
command: Command,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[derive(Debug, Subcommand)]
|
|
22
|
+
enum Command {
|
|
23
|
+
/// Create an example file describing the bentopy input file format with placeholders.
|
|
24
|
+
Example {
|
|
25
|
+
#[arg(short, long, default_value = "example.bent")]
|
|
26
|
+
output: PathBuf,
|
|
27
|
+
},
|
|
28
|
+
/// Check whether an input file is set up correctly and provide lints for common problems.
|
|
29
|
+
///
|
|
30
|
+
/// First, the syntax of the file is validated, before checking for common problems:
|
|
31
|
+
///
|
|
32
|
+
/// There should be at least one compartment.
|
|
33
|
+
/// There should be no duplicate compartment names.
|
|
34
|
+
/// There should be at least one segment.
|
|
35
|
+
/// Segments should refer to compartments that exist.
|
|
36
|
+
/// There should be no duplicate rule names.
|
|
37
|
+
/// Segments should refer to rules that exist.
|
|
38
|
+
Validate {
|
|
39
|
+
#[arg(short, long)]
|
|
40
|
+
input: PathBuf,
|
|
41
|
+
#[arg(short, long, default_value_t)]
|
|
42
|
+
verbose: bool,
|
|
43
|
+
},
|
|
44
|
+
/// Convert from the .json legacy input file format to .bent files.
|
|
45
|
+
///
|
|
46
|
+
/// Note that this command can also read .bent files and produce a .bent file from that. This
|
|
47
|
+
/// can be considered a formatting step.
|
|
48
|
+
Convert {
|
|
49
|
+
#[arg(short, long)]
|
|
50
|
+
input: PathBuf,
|
|
51
|
+
#[arg(short, long)]
|
|
52
|
+
output: PathBuf,
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn main() -> Result<()> {
|
|
57
|
+
let Args { command } = Args::parse();
|
|
58
|
+
match command {
|
|
59
|
+
Command::Example { output } => example(output),
|
|
60
|
+
Command::Validate { input, verbose } => validate(input, verbose),
|
|
61
|
+
Command::Convert { input, output } => convert(input, output),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn example(output: PathBuf) -> Result<()> {
|
|
66
|
+
let example = include_str!("example.bent");
|
|
67
|
+
let mut file = std::fs::File::create(&output)?;
|
|
68
|
+
writeln!(file, "# Created by {BIN_NAME}, version {VERSION}.")?;
|
|
69
|
+
file.write_all(example.as_bytes())?;
|
|
70
|
+
eprintln!("Wrote example file to {output:?}.");
|
|
71
|
+
Ok(())
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn validate(input: PathBuf, verbose: bool) -> Result<()> {
|
|
75
|
+
let mut file = std::fs::File::open(&input)?;
|
|
76
|
+
let mut s = String::new();
|
|
77
|
+
file.read_to_string(&mut s)?;
|
|
78
|
+
|
|
79
|
+
// Try to parse the config.
|
|
80
|
+
let config = Config::parse_bent(&input.to_string_lossy(), &s)
|
|
81
|
+
.context(format!("could not parse {input:?} as a bentopy input file"))?;
|
|
82
|
+
eprintln!("Successfully parsed {input:?}.");
|
|
83
|
+
|
|
84
|
+
// If desired, print a crazy expanded debug struct.
|
|
85
|
+
if verbose {
|
|
86
|
+
eprintln!("Printing debug representation of this configuration:");
|
|
87
|
+
println!("{config:#?}");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// TODO: Write out a nice little report explaining the system. Maybe this is actually a
|
|
91
|
+
// bentopy-init explain thing but whatever. It's a good idea.
|
|
92
|
+
|
|
93
|
+
// TODO: Implement these.
|
|
94
|
+
// eprintln!("NOTE: This is a work-in-progress utility. Notable lacking lints are:");
|
|
95
|
+
// eprintln!(" - check if the referenced structures exist,");
|
|
96
|
+
// eprintln!(" - check if the compartment combinations are well-formed,");
|
|
97
|
+
// eprintln!(" - check if the constraint combinations are well-formed,");
|
|
98
|
+
// eprintln!(" - check if the referenced masks actually exist,");
|
|
99
|
+
// eprintln!(" - check if the referenced masks are of a congruent size,");
|
|
100
|
+
// eprintln!(" - check if the referenced itp includes actually exist,");
|
|
101
|
+
// eprintln!(" - check if the segment names exist in the referenced itps,");
|
|
102
|
+
// eprintln!(" - check if the analytical compartment geometries lie within the dimensions,");
|
|
103
|
+
// eprintln!(" - check for orphan rules and compartments,");
|
|
104
|
+
// eprintln!(" - check if the rules apply within the dimensions.");
|
|
105
|
+
|
|
106
|
+
// Now, we actually check if it is semantically correct.
|
|
107
|
+
let mut problems: usize = 0;
|
|
108
|
+
// There should be at least one compartment.
|
|
109
|
+
if config.compartments.is_empty() {
|
|
110
|
+
println!("Error: No compartments declared. At least one compartment should be declared.");
|
|
111
|
+
problems += 1;
|
|
112
|
+
}
|
|
113
|
+
// There should be no duplicate compartment names.
|
|
114
|
+
let mut compartment_ids = HashSet::with_capacity(config.compartments.len());
|
|
115
|
+
for compartment in &config.compartments {
|
|
116
|
+
let id = compartment.id.as_str();
|
|
117
|
+
if !compartment_ids.insert(id) {
|
|
118
|
+
println!("Warning: Compartment id {id} is defined multiple times.");
|
|
119
|
+
problems += 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// There should be at least one segment.
|
|
123
|
+
if config.compartments.is_empty() {
|
|
124
|
+
println!("Warning: No segments declared. At least one segment should be declared.");
|
|
125
|
+
problems += 1;
|
|
126
|
+
}
|
|
127
|
+
// Segments should refer to compartments that exist.
|
|
128
|
+
for segment in &config.segments {
|
|
129
|
+
let name = segment.name();
|
|
130
|
+
for id in &segment.compartment_ids {
|
|
131
|
+
if !compartment_ids.contains(id.as_str()) {
|
|
132
|
+
println!("Error: Segment {name} refers to an undeclared compartment: {id}.");
|
|
133
|
+
problems += 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// There should be no duplicate rule names.
|
|
138
|
+
let mut rules = HashSet::with_capacity(config.constraints.len());
|
|
139
|
+
for constraint in &config.constraints {
|
|
140
|
+
let id = constraint.id.as_str();
|
|
141
|
+
if !rules.insert(id) {
|
|
142
|
+
println!("Warning: Constraint name {id} is defined multiple times.");
|
|
143
|
+
problems += 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Segments should refer to rules that exist.
|
|
147
|
+
for segment in &config.segments {
|
|
148
|
+
let name = segment.name();
|
|
149
|
+
for id in segment.rules.iter() {
|
|
150
|
+
if !rules.contains(id.as_str()) {
|
|
151
|
+
println!("Error: Segment {name} refers to an undeclared constraint: {id}.");
|
|
152
|
+
problems += 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Report the number of problems.
|
|
158
|
+
match problems {
|
|
159
|
+
0 => println!("It appears there are no problems with this file."),
|
|
160
|
+
1 => println!("Detected one problem."),
|
|
161
|
+
n => println!("Detected {n} problems."),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
Ok(())
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fn convert(input: PathBuf, output: PathBuf) -> Result<()> {
|
|
168
|
+
enum Kind {
|
|
169
|
+
Bent,
|
|
170
|
+
Json,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let kind = match input.extension().and_then(|s| s.to_str()) {
|
|
174
|
+
Some("bent") => Kind::Bent,
|
|
175
|
+
Some("json") => Kind::Json,
|
|
176
|
+
_ => bail!(
|
|
177
|
+
"Cannot read {input:?}. \
|
|
178
|
+
The convert subcommand only supports reading .bent and .json files.",
|
|
179
|
+
),
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if output.extension().is_some_and(|s| s != "bent") {
|
|
183
|
+
bail!(
|
|
184
|
+
"Cannot write configuration to {output:?}. \
|
|
185
|
+
The convert subcommand only supports writing .bent files.",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let mut file = std::fs::File::open(&input)?;
|
|
190
|
+
let mut s = String::new();
|
|
191
|
+
file.read_to_string(&mut s)?;
|
|
192
|
+
|
|
193
|
+
let path = &input.to_string_lossy();
|
|
194
|
+
let config = match kind {
|
|
195
|
+
Kind::Bent => {
|
|
196
|
+
let config = Config::parse_bent(path, &s)
|
|
197
|
+
.context(format!("could not parse {path:?} as a bentopy input file"))?;
|
|
198
|
+
eprintln!("Successfully parsed {path:?}.");
|
|
199
|
+
config
|
|
200
|
+
}
|
|
201
|
+
Kind::Json => {
|
|
202
|
+
let config: legacy::Config = serde_json::from_str(&s).context(format!(
|
|
203
|
+
"could not parse {path:?} as a legacy json input file"
|
|
204
|
+
))?;
|
|
205
|
+
eprintln!("Successfully parsed {path:?} (legacy input file).");
|
|
206
|
+
eprint!("Attempting to convert to new input configuration format... ");
|
|
207
|
+
let config = config.into();
|
|
208
|
+
eprintln!("Done.");
|
|
209
|
+
config
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
eprintln!("Writing to {output:?}.");
|
|
214
|
+
let out = std::fs::File::create(&output)?;
|
|
215
|
+
let mut out = BufWriter::new(out);
|
|
216
|
+
writeln!(out, "# Converted {input:?} to {output:?} using {BIN_NAME}.")?;
|
|
217
|
+
bentopy::core::config::bent::write(&config, &mut out)?;
|
|
218
|
+
|
|
219
|
+
Ok(())
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// TODO: Make this into a method on Segment itself. It would also be nice in bent::writer.
|
|
223
|
+
trait Name {
|
|
224
|
+
fn name(&self) -> Cow<'_, str>;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
impl Name for Segment {
|
|
228
|
+
fn name(&self) -> Cow<'_, str> {
|
|
229
|
+
let name = &self.name;
|
|
230
|
+
match &self.tag {
|
|
231
|
+
Some(tag) => Cow::Owned(format!("{name}:{tag}")),
|
|
232
|
+
None => Cow::Borrowed(name),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
mask/config.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
EPILOG = """
|
|
5
|
+
Compartments are determined using the MDVContainment package created by Bart Bruininks.
|
|
6
|
+
More information is available at <https://github.com/BartBruininks/mdvcontainment>.
|
|
7
|
+
"""
|
|
8
|
+
DESCRIPTION = "Set up masks based on structures and compartment segmentations."
|
|
9
|
+
|
|
10
|
+
DEFAULT_CONTAINMENT_RESOLUTION = 1.0
|
|
11
|
+
DEFAULT_MASK_RESOLUTION = 0.5
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def process_labels(labels: str) -> list[int]:
|
|
15
|
+
return [int(label) for label in labels.split(",")]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def setup_parser(parser=None):
|
|
19
|
+
if parser is None:
|
|
20
|
+
parser = argparse.ArgumentParser(
|
|
21
|
+
description=DESCRIPTION,
|
|
22
|
+
epilog=EPILOG,
|
|
23
|
+
)
|
|
24
|
+
else:
|
|
25
|
+
parser.description = DESCRIPTION
|
|
26
|
+
parser.epilog = EPILOG
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"input",
|
|
29
|
+
type=Path,
|
|
30
|
+
help="Input structure file to subject to segmentation.",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"output",
|
|
34
|
+
type=Path,
|
|
35
|
+
nargs="?",
|
|
36
|
+
help="Output path for the resulting voxel mask.",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--no-interactive",
|
|
40
|
+
dest="interactive",
|
|
41
|
+
action="store_false",
|
|
42
|
+
help="""Do not ask for command line input at runtime. May be desirable
|
|
43
|
+
when all inputs are known and in a scripted context.
|
|
44
|
+
(normally interactive)""",
|
|
45
|
+
)
|
|
46
|
+
morph = parser.add_mutually_exclusive_group()
|
|
47
|
+
morph.add_argument(
|
|
48
|
+
"--morph",
|
|
49
|
+
type=str,
|
|
50
|
+
help="""Morphological operations to apply to the initial boolean voxel
|
|
51
|
+
representation based on the provided structure. Provide a string of 'd'
|
|
52
|
+
for dilation steps and 'e' for erosion steps, in the order you wish to
|
|
53
|
+
apply them.
|
|
54
|
+
|
|
55
|
+
For example, the string 'de' is equivalent to the --closing flag, while
|
|
56
|
+
'ed' is a morphological opening operation. See
|
|
57
|
+
<https://en.wikipedia.org/wiki/Closing_(morphology)> for more
|
|
58
|
+
information.
|
|
59
|
+
""",
|
|
60
|
+
)
|
|
61
|
+
morph.add_argument(
|
|
62
|
+
"--closing",
|
|
63
|
+
action="store_true",
|
|
64
|
+
help="""Use binary closing to fill small holes in compartments. For
|
|
65
|
+
analysis of CG Martini structures with a voxel resolution 0.5 nm this
|
|
66
|
+
is highly recommended.
|
|
67
|
+
|
|
68
|
+
If the resolution exceeds the condensed phase distance (i.e., about
|
|
69
|
+
double the LJ sigma), closing is not required. A voxel resolution below
|
|
70
|
+
sigma is not recommended.
|
|
71
|
+
|
|
72
|
+
See also, --morph, which provides a more flexible interface to the same
|
|
73
|
+
notion.
|
|
74
|
+
""",
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--min-size",
|
|
78
|
+
type=float,
|
|
79
|
+
help="""
|
|
80
|
+
A volume in nm³. Starting from the leaf nodes, recursively merge with
|
|
81
|
+
ancestors until this minimum size has been met.
|
|
82
|
+
|
|
83
|
+
This is very helpful for filtering out small, non-relevant compartments
|
|
84
|
+
by merging them to their parent compartments.
|
|
85
|
+
""",
|
|
86
|
+
)
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"--containment-resolution",
|
|
89
|
+
default=DEFAULT_CONTAINMENT_RESOLUTION,
|
|
90
|
+
type=float,
|
|
91
|
+
help="Resolution for the compartment finding routine (nm). (default: %(default)s nm)",
|
|
92
|
+
)
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"--mask-resolution",
|
|
95
|
+
default=DEFAULT_MASK_RESOLUTION,
|
|
96
|
+
type=float,
|
|
97
|
+
help="Voxel size (resolution) for the exported mask (nm). (default: %(default)s nm)",
|
|
98
|
+
)
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
"--selection",
|
|
101
|
+
default="not resname W ION",
|
|
102
|
+
type=str,
|
|
103
|
+
help="MDAnalysis selection string for the atom group over which to perform the segmentation. (default: '%(default)s')",
|
|
104
|
+
)
|
|
105
|
+
labels_group = parser.add_mutually_exclusive_group()
|
|
106
|
+
labels_group.add_argument(
|
|
107
|
+
"--labels",
|
|
108
|
+
type=process_labels,
|
|
109
|
+
help="Pre-selected compartment labels, comma-separated.",
|
|
110
|
+
)
|
|
111
|
+
labels_group.add_argument(
|
|
112
|
+
"--autofill",
|
|
113
|
+
action="store_true",
|
|
114
|
+
help="Automatically select the leaf nodes from the containment graph, which commonly represent the 'insides' of the system.",
|
|
115
|
+
)
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"-b",
|
|
118
|
+
"--inspect-labels-path",
|
|
119
|
+
type=Path,
|
|
120
|
+
nargs="?",
|
|
121
|
+
const="labels.gro",
|
|
122
|
+
help="""Optional output path to a gro file to write labeled voxel positions to.
|
|
123
|
+
This file can be inspected with molecule viewers, which is very helpful
|
|
124
|
+
in determining which labels match the compartments you want to select.
|
|
125
|
+
(when used, default: %(const)s)""",
|
|
126
|
+
)
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
"--debug-voxels",
|
|
129
|
+
type=Path,
|
|
130
|
+
help="""Write the final voxel mask as a gro file for inspection with molecule viewers.
|
|
131
|
+
This can be useful when you want to verify the voxel mask that is produced for some selection of labels.""",
|
|
132
|
+
)
|
|
133
|
+
parser.add_argument(
|
|
134
|
+
"--no-cache",
|
|
135
|
+
action="store_true",
|
|
136
|
+
help="""Do not cache structure files.
|
|
137
|
+
|
|
138
|
+
Caching can improve structure load times a lot, because it can load a
|
|
139
|
+
previously stored cache of a MDA Universe. Since loading structure
|
|
140
|
+
files with MDA can be very slow and loading the pickled object is
|
|
141
|
+
relatively quick, this is fantastic for making multiple different
|
|
142
|
+
masks of the same structure.
|
|
143
|
+
|
|
144
|
+
This option allows you to *switch off* the caching, if you want that.
|
|
145
|
+
""",
|
|
146
|
+
)
|
|
147
|
+
parser.add_argument(
|
|
148
|
+
"--verbose",
|
|
149
|
+
"-v",
|
|
150
|
+
action="store_true",
|
|
151
|
+
help="Display verbose output.",
|
|
152
|
+
)
|
|
153
|
+
return parser
|
mask/mask.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
import pickle
|
|
3
|
+
from time import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import MDAnalysis as mda
|
|
7
|
+
import numpy as np
|
|
8
|
+
from mdvcontainment import Containment
|
|
9
|
+
from mdvcontainment.voxel_logic import voxels_to_universe
|
|
10
|
+
|
|
11
|
+
from .config import setup_parser
|
|
12
|
+
from .utilities import voxels_to_gro
|
|
13
|
+
|
|
14
|
+
# Let's ignore the wordy warnings we tend to get from MDAnalysis.
|
|
15
|
+
warnings.filterwarnings("ignore")
|
|
16
|
+
|
|
17
|
+
log = print
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def mask(args):
|
|
21
|
+
# First, we want to verify our arguments.
|
|
22
|
+
# Down the line, we assume that the containment resolution can be treated
|
|
23
|
+
# as integer multiple of the mask resolution. Let's check that at the top.
|
|
24
|
+
if args.containment_resolution % args.mask_resolution > 0.01:
|
|
25
|
+
log("ERROR: The containment and mask resolutions are not well-formed.")
|
|
26
|
+
log(
|
|
27
|
+
f"The containment resolution ({args.containment_resolution})",
|
|
28
|
+
f"must be a multiple of the mask resolution ({args.mask_resolution}),",
|
|
29
|
+
"such that `containment_resolution % mask resolution == 0`.",
|
|
30
|
+
)
|
|
31
|
+
return 1
|
|
32
|
+
if args.mask_resolution < 0:
|
|
33
|
+
log(f"ERROR: The mask resolution ({args.mask_resolution}) cannot be negative.")
|
|
34
|
+
return 1
|
|
35
|
+
zoom = int(args.containment_resolution / args.mask_resolution)
|
|
36
|
+
# Everything must be set correctly in case --no-interactive is used.
|
|
37
|
+
if not args.interactive:
|
|
38
|
+
log("Running in non-interactive mode.")
|
|
39
|
+
if args.output is None:
|
|
40
|
+
log("WARNING: No mask output path was specified.")
|
|
41
|
+
log("The computed mask will not be written to disk.")
|
|
42
|
+
log("Like tears in rain.")
|
|
43
|
+
if args.labels is None and not args.autofill:
|
|
44
|
+
log("ERROR: No labels were specified.")
|
|
45
|
+
log("In non-interactive mode, at least one label must be provided manually (`--label`) or automatically (`--autofill`).")
|
|
46
|
+
return 1
|
|
47
|
+
|
|
48
|
+
# Read in structures from a structure file or from a cache..
|
|
49
|
+
u = None
|
|
50
|
+
caching = not args.no_cache # Whether caching is enabled.
|
|
51
|
+
structure_path = args.input
|
|
52
|
+
if caching:
|
|
53
|
+
cached_path = resolve_cache_path(structure_path)
|
|
54
|
+
|
|
55
|
+
# We need to check if the requested structure file actually exists.
|
|
56
|
+
# We don't want to end up using a cached file for a structure file that
|
|
57
|
+
# does not exist anymore.
|
|
58
|
+
structure_exists = structure_path.is_file()
|
|
59
|
+
# Try to find a cached equivalent to the structure.
|
|
60
|
+
cache_exists = cached_path.is_file()
|
|
61
|
+
if structure_exists and cache_exists:
|
|
62
|
+
log(f"Reading in cached structure from {cached_path}...")
|
|
63
|
+
log("You can reload a changed structure by removing the cached file.")
|
|
64
|
+
log("Caching can be disabled with --no-cache.")
|
|
65
|
+
with open(cached_path, "rb") as cache_file:
|
|
66
|
+
start = time()
|
|
67
|
+
u = pickle.load(cache_file)
|
|
68
|
+
dur = time() - start
|
|
69
|
+
log(
|
|
70
|
+
f"Done reading the cached file. (Read {u.atoms.n_atoms} atoms in {dur:.1f} s.)"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# If the file was not cached, we read it from the structure file.
|
|
74
|
+
if u is None:
|
|
75
|
+
# Read the structure file.
|
|
76
|
+
log(f"Reading in structure from {structure_path}... ", end="")
|
|
77
|
+
start = time()
|
|
78
|
+
u = mda.Universe(structure_path)
|
|
79
|
+
dur = time() - start
|
|
80
|
+
log(f"done. (Read {u.atoms.n_atoms} atoms in {dur:.1f} s.)")
|
|
81
|
+
|
|
82
|
+
# Cache the universe if desired and necessary.
|
|
83
|
+
if caching and not cache_exists:
|
|
84
|
+
with open(cached_path, "wb") as cache_file:
|
|
85
|
+
pickle.dump(u, cache_file)
|
|
86
|
+
|
|
87
|
+
# Apply the selection before we hand it to mdvcontainment.
|
|
88
|
+
selection = u.select_atoms(args.selection)
|
|
89
|
+
log(f"Selected {selection.n_atoms} atoms according to '{args.selection}'.")
|
|
90
|
+
|
|
91
|
+
# Closing is just a short-hand for --morph='de'.
|
|
92
|
+
morph = args.morph
|
|
93
|
+
if args.closing:
|
|
94
|
+
morph = "de"
|
|
95
|
+
# MDVContainment only accepts str, not str | None as of 2.0.0a2.
|
|
96
|
+
if morph == None:
|
|
97
|
+
morph = ""
|
|
98
|
+
|
|
99
|
+
# Calculate the containments. This is all in the hands of mdvcontainment.
|
|
100
|
+
log("Calculating containment... ", end="")
|
|
101
|
+
start = time()
|
|
102
|
+
containment = Containment(
|
|
103
|
+
selection,
|
|
104
|
+
resolution=args.containment_resolution,
|
|
105
|
+
morph=morph,
|
|
106
|
+
max_offset=0, # We accept any result of voxelization.
|
|
107
|
+
verbose=args.verbose,
|
|
108
|
+
no_mapping=True, # Mapping takes some time and is not used at all in this context.
|
|
109
|
+
)
|
|
110
|
+
duration = time() - start
|
|
111
|
+
log(f"Done in {duration:.3} s.")
|
|
112
|
+
|
|
113
|
+
if args.min_size is not None:
|
|
114
|
+
log(f"Applying a {args.min_size} nm³ minimum size view.")
|
|
115
|
+
containment = containment.node_view(min_size=args.min_size)
|
|
116
|
+
|
|
117
|
+
# Show what we found.
|
|
118
|
+
log("Found the following node groups:")
|
|
119
|
+
log(f" root:\t{npc(containment.voxel_containment.root_nodes)}")
|
|
120
|
+
log(f" leaf:\t{npc(containment.voxel_containment.leaf_nodes)}")
|
|
121
|
+
# And show it as a tree of containments.
|
|
122
|
+
print(containment.voxel_containment)
|
|
123
|
+
|
|
124
|
+
# Get the label array.
|
|
125
|
+
label_array = containment.voxel_containment.components_grid
|
|
126
|
+
# Write the labels to a gro file if desired.
|
|
127
|
+
if args.inspect_labels_path is None and args.interactive:
|
|
128
|
+
log("Do you want to write a label map as a gro file to view the containments?")
|
|
129
|
+
log("Provide an output path. To skip this step, leave this field empty.")
|
|
130
|
+
while True:
|
|
131
|
+
path = input("(gro) -> ").strip()
|
|
132
|
+
if len(path) == 0:
|
|
133
|
+
labels_path = None
|
|
134
|
+
break
|
|
135
|
+
labels_path = Path(path)
|
|
136
|
+
if labels_path.suffix == ".gro":
|
|
137
|
+
break
|
|
138
|
+
log(f"Path must have a gro extension. Found '{labels_path.suffix}'.")
|
|
139
|
+
else:
|
|
140
|
+
labels_path = args.inspect_labels_path
|
|
141
|
+
root_nodes = npc(containment.voxel_containment.root_nodes)
|
|
142
|
+
log(f"Do you want to exclude the 'outside' compartment(s) (labels {root_nodes})?")
|
|
143
|
+
log("Excluding the root nodes is helpful, because they usually make up a large part of the space.")
|
|
144
|
+
while True:
|
|
145
|
+
answer = input("(Y/n) -> ").strip()
|
|
146
|
+
match answer.lower():
|
|
147
|
+
case "" | "y":
|
|
148
|
+
exclude_outside = True
|
|
149
|
+
break
|
|
150
|
+
case "n":
|
|
151
|
+
exclude_outside = False
|
|
152
|
+
break
|
|
153
|
+
case _: log(f"Expected 'y' or 'n'. Found '{answer}'.")
|
|
154
|
+
if labels_path is not None:
|
|
155
|
+
# voxels_to_gro(labels_path, label_array, scale=args.containment_resolution)
|
|
156
|
+
if exclude_outside:
|
|
157
|
+
nodes = set(containment.voxel_containment.nodes) - set(containment.voxel_containment.root_nodes)
|
|
158
|
+
else:
|
|
159
|
+
nodes = containment.voxel_containment.nodes
|
|
160
|
+
labels = containment.voxel_containment.components_grid
|
|
161
|
+
labels_u = voxels_to_universe(labels, nodes=list(nodes), universe=u)
|
|
162
|
+
assert labels_u is not None
|
|
163
|
+
assert labels_u.atoms is not None
|
|
164
|
+
|
|
165
|
+
if len(labels_u.atoms) > 0:
|
|
166
|
+
log(f"Writing labels voxels debug file to {labels_path}... ", end="")
|
|
167
|
+
labels_u.atoms.write(labels_path)
|
|
168
|
+
log("done.")
|
|
169
|
+
else:
|
|
170
|
+
log(f"Could not write labels to {labels_path}, because there are no voxels to write!")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# Let's select our labels.
|
|
174
|
+
possible_labels = npc(containment.voxel_containment.nodes)
|
|
175
|
+
if args.interactive and args.labels is None and args.autofill is False:
|
|
176
|
+
log(
|
|
177
|
+
"No compartment labels have been selected, yet.",
|
|
178
|
+
"Select one or more to continue.\n",
|
|
179
|
+
f"Options: {possible_labels}.",
|
|
180
|
+
"Provide them as a space-separated list followed by a return.",
|
|
181
|
+
)
|
|
182
|
+
labels = []
|
|
183
|
+
while len(labels) == 0:
|
|
184
|
+
try:
|
|
185
|
+
labels.extend(map(lambda label: int(label), input("-> ").split()))
|
|
186
|
+
except ValueError:
|
|
187
|
+
log("Could not parse the labels. Make sure they are well-formed.")
|
|
188
|
+
for label in labels:
|
|
189
|
+
if label not in possible_labels:
|
|
190
|
+
log(f"'{label}' is not a valid compartment label.")
|
|
191
|
+
log("Please try again.")
|
|
192
|
+
labels.clear()
|
|
193
|
+
break
|
|
194
|
+
elif args.autofill is True:
|
|
195
|
+
labels = containment.voxel_containment.leaf_nodes
|
|
196
|
+
log(f"Automatically choosing leaf components: {npc(labels)}.")
|
|
197
|
+
else:
|
|
198
|
+
# Making sure we this is the case, even though we do this check at the top as well.
|
|
199
|
+
assert (
|
|
200
|
+
args.labels is not None
|
|
201
|
+
), "No labels are specified and the mode is non-interactive so we can't ask."
|
|
202
|
+
labels = args.labels
|
|
203
|
+
for label in labels:
|
|
204
|
+
if label not in possible_labels:
|
|
205
|
+
log(f"ERROR: '{label}' is not a valid compartment label.")
|
|
206
|
+
return 1
|
|
207
|
+
log(f"Selected the following labels: {npc(labels)}.")
|
|
208
|
+
|
|
209
|
+
# Get our compartment by masking out all voxels that have our selected labels.
|
|
210
|
+
compartment = containment.voxel_containment.get_voxel_mask(labels)
|
|
211
|
+
full = np.count_nonzero(compartment == True)
|
|
212
|
+
free = np.count_nonzero(compartment == False)
|
|
213
|
+
full_volume = full * containment.voxel_volume
|
|
214
|
+
free_volume = free * containment.voxel_volume
|
|
215
|
+
total_volume = full_volume + free_volume
|
|
216
|
+
full_frac = full_volume / total_volume
|
|
217
|
+
free_frac = free_volume / total_volume
|
|
218
|
+
log("Selected compartment contains:")
|
|
219
|
+
log(f" occupied:\t{full} voxels\t({full_frac:.1%}, {full_volume:.1f} nm³)")
|
|
220
|
+
log(f" available:\t{free} voxels\t({free_frac:.1%}, {free_volume:.1f} nm³)")
|
|
221
|
+
|
|
222
|
+
# Produce our final output mask according to the specified output mask resolution.
|
|
223
|
+
log(f"Output mask resolution is set to {args.mask_resolution} nm.")
|
|
224
|
+
log(f"Zoom factor from containment voxels to mask voxels is {zoom}.")
|
|
225
|
+
zoomed = compartment.repeat(zoom, axis=0).repeat(zoom, axis=1).repeat(zoom, axis=2)
|
|
226
|
+
|
|
227
|
+
# Report a summary of the final mask's dimensions.
|
|
228
|
+
mask_res = args.mask_resolution
|
|
229
|
+
mask_shape = zoomed.shape
|
|
230
|
+
mask_size = tuple(npc(np.array(mask_shape) * mask_res))
|
|
231
|
+
log(f"Size of final voxel mask is {mask_shape} at a {mask_res} nm resolution.")
|
|
232
|
+
log(f"This corresponds to a final mask size of {mask_size} nm.")
|
|
233
|
+
|
|
234
|
+
# Write out a debug voxels gro of the mask if desired.
|
|
235
|
+
if args.debug_voxels is None and args.interactive:
|
|
236
|
+
log("Do you want to write the voxel mask as a gro file to inspect it?")
|
|
237
|
+
log("Warning: This file may be quite large, depending on the mask resolution.")
|
|
238
|
+
log("Provide an output path. To skip this step, leave this field empty.")
|
|
239
|
+
while True:
|
|
240
|
+
path = input("(gro) -> ").strip()
|
|
241
|
+
if len(path) == 0:
|
|
242
|
+
voxels_path = None
|
|
243
|
+
break
|
|
244
|
+
voxels_path = Path(path)
|
|
245
|
+
if voxels_path.suffix == ".gro":
|
|
246
|
+
break
|
|
247
|
+
log(f"Path must have a gro extension. Found '{voxels_path.suffix}'.")
|
|
248
|
+
else:
|
|
249
|
+
voxels_path = args.debug_voxels
|
|
250
|
+
if voxels_path is not None:
|
|
251
|
+
log(f"Writing mask voxels debug file to {voxels_path}... ", end="")
|
|
252
|
+
voxels_to_universe(zoomed, universe=u, nodes=[True])
|
|
253
|
+
voxels_to_gro(voxels_path, zoomed, scale=args.mask_resolution)
|
|
254
|
+
log("done.")
|
|
255
|
+
|
|
256
|
+
# Determine the voxel mask output path.
|
|
257
|
+
output_path = args.output
|
|
258
|
+
if args.output is None and args.interactive:
|
|
259
|
+
log("Please provide an output path for the final voxel mask.")
|
|
260
|
+
while True:
|
|
261
|
+
path = input("(npz) -> ").strip()
|
|
262
|
+
if len(path) == 0:
|
|
263
|
+
output_path = None
|
|
264
|
+
break
|
|
265
|
+
output_path = Path(path)
|
|
266
|
+
if output_path.suffix == ".npz":
|
|
267
|
+
break
|
|
268
|
+
log(f"Path must have an npz extension. Found '{output_path.suffix}'.")
|
|
269
|
+
|
|
270
|
+
# Finally, write out the voxel mask when desired.
|
|
271
|
+
if output_path is not None:
|
|
272
|
+
log(f"Writing the voxel mask to {output_path}... ", end="")
|
|
273
|
+
np.savez(output_path, zoomed)
|
|
274
|
+
log("done.")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def resolve_cache_path(structure_path):
|
|
278
|
+
"""
|
|
279
|
+
Determine what the cache path for the provided structure path is.
|
|
280
|
+
"""
|
|
281
|
+
structure_name = structure_path.name
|
|
282
|
+
cached_name = f"#cached_{structure_name}.pickle"
|
|
283
|
+
return structure_path.with_name(cached_name)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def npc_single(n):
|
|
287
|
+
match n:
|
|
288
|
+
case np.float32() | np.float64() | float():
|
|
289
|
+
return float(n)
|
|
290
|
+
case np.int32() | np.int64() | int():
|
|
291
|
+
return int(n)
|
|
292
|
+
case _:
|
|
293
|
+
print(f"WARNING: Unknown type for {n}: {type(n)}")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def npc(ns):
|
|
297
|
+
"Canonicalize numpy number types to print them properly."
|
|
298
|
+
return [npc_single(n) for n in ns]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def main():
|
|
302
|
+
parser = setup_parser()
|
|
303
|
+
args = parser.parse_args()
|
|
304
|
+
return mask(args)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
if __name__ == "__main__":
|
|
308
|
+
main()
|