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
core/config.rs ADDED
@@ -0,0 +1,362 @@
1
+ use anyhow::Result;
2
+
3
+ use std::path::PathBuf;
4
+
5
+ pub mod bent;
6
+ mod compartment_combinations;
7
+ pub mod legacy;
8
+
9
+ pub mod defaults {
10
+ pub const TITLE: &str = "Bentopy system";
11
+ /// Bead radius for voxelization in nm.
12
+ pub const BEAD_RADIUS: f64 = 0.20;
13
+ pub const PERIODIC: bool = true;
14
+ pub const MAX_TRIES_MULT: u64 = 1000;
15
+ pub const MAX_TRIES_ROT_DIV: u64 = 100;
16
+ }
17
+
18
+ /// Avogadro's number (per mol).
19
+ const N_A: f64 = 6.0221415e23;
20
+
21
+ pub type CompartmentID = String;
22
+ pub type Dimensions = [f32; 3];
23
+ pub type Point = [f32; 3];
24
+
25
+ impl Config {
26
+ /// Parse a `.bent` input file.
27
+ pub fn parse_bent(path: &str, s: &str) -> Result<Self> {
28
+ bent::parse_config(path, s)
29
+ }
30
+
31
+ /// Parse a legacy `.json` input file.
32
+ pub fn parse_legacy_json(_s: &str) -> Result<Self> {
33
+ todo!()
34
+ }
35
+ }
36
+
37
+ #[derive(Debug, Default, Clone, PartialEq)]
38
+ pub enum RearrangeMethod {
39
+ #[default]
40
+ Moment,
41
+ Volume,
42
+ BoundingSphere,
43
+ None,
44
+ }
45
+
46
+ #[derive(Debug, Default, PartialEq)]
47
+ pub struct General {
48
+ pub title: Option<String>,
49
+ pub seed: Option<u64>,
50
+ pub bead_radius: Option<f64>,
51
+ pub max_tries_mult: Option<u64>,
52
+ pub max_tries_rot_div: Option<u64>,
53
+ pub rearrange_method: Option<RearrangeMethod>,
54
+ }
55
+
56
+ #[derive(Debug, Default, PartialEq)]
57
+ pub struct Space {
58
+ pub dimensions: Option<Dimensions>,
59
+ pub resolution: Option<f64>,
60
+ pub periodic: Option<bool>,
61
+ }
62
+
63
+ #[derive(Debug, Clone, PartialEq, Eq)]
64
+ #[allow(dead_code)]
65
+ pub enum Expr<T> {
66
+ Term(T),
67
+ Not(Box<Self>),
68
+ Or(Box<Self>, Box<Self>),
69
+ And(Box<Self>, Box<Self>),
70
+ }
71
+
72
+ impl<T: std::fmt::Display> std::fmt::Display for Expr<T> {
73
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74
+ match self {
75
+ Self::Term(t) => t.fmt(f),
76
+ Self::Not(expr) => write!(f, "not {expr}"),
77
+ Self::Or(l, r) => write!(f, "({l} or {r})"),
78
+ Self::And(l, r) => write!(f, "({l} and {r})"),
79
+ }
80
+ }
81
+ }
82
+
83
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
84
+ pub enum Axis {
85
+ X,
86
+ Y,
87
+ Z,
88
+ }
89
+
90
+ impl TryFrom<char> for Axis {
91
+ type Error = &'static str;
92
+
93
+ fn try_from(c: char) -> Result<Self, Self::Error> {
94
+ match c {
95
+ 'x' => Ok(Self::X),
96
+ 'y' => Ok(Self::Y),
97
+ 'z' => Ok(Self::Z),
98
+ _ => Err("unknown axis"),
99
+ }
100
+ }
101
+ }
102
+
103
+ impl std::fmt::Display for Axis {
104
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105
+ match self {
106
+ Self::X => 'x',
107
+ Self::Y => 'y',
108
+ Self::Z => 'z',
109
+ }
110
+ .fmt(f)
111
+ }
112
+ }
113
+
114
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
115
+ pub enum Op {
116
+ LessThan,
117
+ GreaterThan,
118
+ }
119
+
120
+ impl Op {
121
+ fn reverse(self) -> Self {
122
+ match self {
123
+ Self::LessThan => Self::GreaterThan,
124
+ Self::GreaterThan => Self::LessThan,
125
+ }
126
+ }
127
+ }
128
+
129
+ impl TryFrom<char> for Op {
130
+ type Error = &'static str;
131
+
132
+ fn try_from(c: char) -> Result<Self, Self::Error> {
133
+ match c {
134
+ '<' => Ok(Self::LessThan),
135
+ '>' => Ok(Self::GreaterThan),
136
+ _ => Err("unknown operator"),
137
+ }
138
+ }
139
+ }
140
+
141
+ impl std::fmt::Display for Op {
142
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143
+ match self {
144
+ Self::LessThan => '<',
145
+ Self::GreaterThan => '>',
146
+ }
147
+ .fmt(f)
148
+ }
149
+ }
150
+
151
+ #[derive(Debug, Clone, Copy, PartialEq)]
152
+ pub struct Limit {
153
+ pub axis: Axis,
154
+ pub op: Op,
155
+ pub value: f64,
156
+ }
157
+
158
+ impl std::fmt::Display for Limit {
159
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160
+ let Limit { axis, op, value } = self;
161
+ write!(f, "{axis} {op} {value}")
162
+ }
163
+ }
164
+
165
+ #[derive(Debug, Clone, Copy, PartialEq)]
166
+ pub enum Quantity {
167
+ /// Copy number.
168
+ Number(u64),
169
+ /// Molarity (mol/L).
170
+ Concentration(f64),
171
+ }
172
+
173
+ impl Quantity {
174
+ /// Determine the number of segments that is implied by this [`Quantity`].
175
+ ///
176
+ /// In case this `Quantity` is a [`Quantity::Concentration`], the number of segments is
177
+ /// lazily determined from the provided `volume`, and rounded.
178
+ ///
179
+ /// The value returned by `volume` must be in cubic nanometers (nm³).
180
+ pub fn bake<F: Fn() -> f64>(&self, volume: F) -> u64 {
181
+ match *self {
182
+ Quantity::Number(n) => n,
183
+ Quantity::Concentration(c) => {
184
+ let v = volume() * 1e-24; // From nm³ to L.
185
+ let n = N_A * c * v;
186
+ f64::round(n) as u64
187
+ }
188
+ }
189
+ }
190
+
191
+ /// Returns whether the contained value can be interpreted as resulting in zero placements.
192
+ ///
193
+ /// When the quantity is a `Number(0)` or `Concentration(0.0)`, the baked number is certainly
194
+ /// zero. When `Number(n)` for `n > 0`, the baked number is certainly not zero.
195
+ ///
196
+ /// But, in case of a positive concentration, whether the final number is zero or not depends
197
+ /// on the associated volume.
198
+ ///
199
+ /// If the concentration is smaller than zero, it is treated as a zero.
200
+ pub fn is_zero(&self) -> bool {
201
+ match *self {
202
+ Quantity::Number(n) => n == 0,
203
+ Quantity::Concentration(c) => c <= 0.0,
204
+ }
205
+ }
206
+ }
207
+
208
+ impl std::fmt::Display for Quantity {
209
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210
+ match self {
211
+ Self::Number(count) => count.fmt(f),
212
+ Self::Concentration(conc) => write!(f, "{conc}M"),
213
+ }
214
+ }
215
+ }
216
+
217
+ #[derive(Debug, Clone, PartialEq)]
218
+ pub struct Segment {
219
+ pub name: String,
220
+ pub tag: Option<String>, // Should be 5-character ArrayString.
221
+ pub quantity: Quantity,
222
+ pub path: PathBuf,
223
+ pub compartment_ids: Box<[String]>,
224
+ pub rules: Box<[String]>,
225
+ }
226
+
227
+ #[derive(Debug, Clone, PartialEq)]
228
+ pub struct Compartment {
229
+ pub id: CompartmentID,
230
+ pub mask: Mask,
231
+ }
232
+
233
+ impl Compartment {
234
+ /// Returns whether this [`Compartment`] is dependent on other compartments.
235
+ pub fn is_predefined(&self) -> bool {
236
+ match &self.mask {
237
+ Mask::All | Mask::Voxels(_) | Mask::Shape(_) | Mask::Limits(_) => true,
238
+ Mask::Within { .. } | Mask::Combination(_) => false,
239
+ }
240
+ }
241
+ }
242
+
243
+ #[derive(Debug, Clone, PartialEq)]
244
+ pub enum Mask {
245
+ All,
246
+ Voxels(PathBuf),
247
+ Shape(Shape),
248
+ Limits(Expr<Limit>),
249
+ // These are constructed by referencing previously defined masks.
250
+ Within { distance: f32, id: CompartmentID },
251
+ Combination(Expr<CompartmentID>),
252
+ }
253
+
254
+ #[derive(Debug, Clone, PartialEq)]
255
+ pub enum Shape {
256
+ Sphere { center: Anchor, radius: f32 }, // Consider the f64 situation.
257
+ Cuboid { start: Anchor, end: Anchor },
258
+ // TODO: More?
259
+ }
260
+
261
+ impl std::fmt::Display for Shape {
262
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
263
+ match self {
264
+ Self::Sphere { center, radius } => {
265
+ write!(f, "sphere at {center} with radius {radius}")
266
+ }
267
+ Self::Cuboid { start, end } => write!(f, "cuboid from {start} to {end}"),
268
+ }
269
+ }
270
+ }
271
+
272
+ #[derive(Debug, Clone, Copy, PartialEq)]
273
+ pub enum Anchor {
274
+ Start,
275
+ Center,
276
+ End,
277
+ Point(Point),
278
+ }
279
+
280
+ impl std::fmt::Display for Anchor {
281
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282
+ match self {
283
+ Self::Start => "start".fmt(f),
284
+ Self::Center => "center".fmt(f),
285
+ Self::End => "end".fmt(f),
286
+ Self::Point([x, y, z]) => write!(f, "{x}, {y}, {z}"),
287
+ }
288
+ }
289
+ }
290
+
291
+ #[derive(Debug, Clone, PartialEq, Eq, Hash)]
292
+ pub struct Constraint {
293
+ pub id: String,
294
+ pub rule: Rule,
295
+ }
296
+
297
+ #[derive(Debug, Clone, PartialEq, Eq, Hash)]
298
+ pub enum Rule {
299
+ RotationAxes(Axes),
300
+ }
301
+
302
+ #[derive(Debug, PartialEq)]
303
+ pub struct Config {
304
+ pub general: General,
305
+ pub space: Space,
306
+ pub includes: Vec<PathBuf>,
307
+ pub constraints: Vec<Constraint>,
308
+ pub compartments: Vec<Compartment>,
309
+ pub segments: Vec<Segment>,
310
+ }
311
+
312
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
313
+ pub struct Axes {
314
+ pub x: bool,
315
+ pub y: bool,
316
+ pub z: bool,
317
+ }
318
+
319
+ impl Default for Axes {
320
+ fn default() -> Self {
321
+ Self {
322
+ x: true,
323
+ y: true,
324
+ z: true,
325
+ }
326
+ }
327
+ }
328
+
329
+ impl Axes {
330
+ pub fn list(&self) -> Box<[Axis]> {
331
+ let mut v = Vec::with_capacity(3);
332
+ if self.x {
333
+ v.push(Axis::X);
334
+ }
335
+ if self.y {
336
+ v.push(Axis::Y);
337
+ }
338
+ if self.z {
339
+ v.push(Axis::Z);
340
+ }
341
+ v.into_boxed_slice()
342
+ }
343
+ }
344
+
345
+ impl std::str::FromStr for Axes {
346
+ type Err = String;
347
+
348
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
349
+ let len = s.len();
350
+ if len > 3 {
351
+ return Err(format!(
352
+ "an axes string may consist of at most 3 characters, '{s}' has {len} characters"
353
+ ));
354
+ }
355
+
356
+ Ok(Self {
357
+ x: s.contains('x'),
358
+ y: s.contains('y'),
359
+ z: s.contains('z'),
360
+ })
361
+ }
362
+ }
core/mod.rs ADDED
@@ -0,0 +1,4 @@
1
+ pub mod config;
2
+ pub mod placement;
3
+ pub mod utilities;
4
+ pub mod version;
core/placement.rs ADDED
@@ -0,0 +1,100 @@
1
+ use std::path::PathBuf;
2
+
3
+ use glam::Mat3;
4
+ use serde::{Deserialize, Serialize};
5
+
6
+ use crate::core::config::Dimensions;
7
+
8
+ type Rotation = Mat3;
9
+ type Position = [f32; 3];
10
+ type RowMajorRotation = [[f32; 3]; 3];
11
+
12
+ /// A list of instance-based [`Placement`]s of structures in a space.
13
+ #[derive(Debug, Clone, Serialize, Deserialize)]
14
+ pub struct PlacementList {
15
+ pub title: String,
16
+ pub size: Dimensions,
17
+ #[serde(flatten)]
18
+ pub meta: Option<Meta>,
19
+ pub topol_includes: Vec<String>,
20
+ pub placements: Vec<Placement>,
21
+ }
22
+
23
+ /// Additional information about the packed structure that is stored in the [`PlacementList`].
24
+ #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
25
+ pub struct Meta {
26
+ pub seed: u64,
27
+ pub max_tries_mult: u64,
28
+ pub max_tries_per_rotation_divisor: u64,
29
+ pub bead_radius: f32,
30
+ }
31
+
32
+ /// A set of positions associated with some structure.
33
+ ///
34
+ /// The structure is named and is stored at the provided `path`.
35
+ ///
36
+ /// Positions are stored in [`Batch`]es.
37
+ #[derive(Debug, Clone, Serialize, Deserialize)]
38
+ pub struct Placement {
39
+ pub name: String,
40
+ /// A tag that will replace the associated structure's `resname` field, if present.
41
+ pub tag: Option<String>,
42
+ /// Path to the segment's molecule file.
43
+ pub path: PathBuf,
44
+ pub batches: Vec<Batch>,
45
+ }
46
+
47
+ impl Placement {
48
+ /// Creates a new [`Placement`].
49
+ pub fn new(name: String, tag: Option<String>, path: PathBuf) -> Self {
50
+ Self {
51
+ name,
52
+ tag,
53
+ path,
54
+ batches: Default::default(),
55
+ }
56
+ }
57
+
58
+ /// Returns the optional tag of this [`Placement`].
59
+ pub fn tag(&self) -> Option<&str> {
60
+ self.tag.as_deref()
61
+ }
62
+
63
+ /// Push a new [`Batch`] onto this [`Placement`].
64
+ pub fn push(&mut self, batch: Batch) {
65
+ self.batches.push(batch)
66
+ }
67
+ }
68
+
69
+ /// A set of positions that share a specific rotation.
70
+ #[derive(Debug, Clone, Serialize, Deserialize)]
71
+ pub struct Batch {
72
+ /// Rotations are stored in row-major order, since they are stored like that in the placement
73
+ /// list.
74
+ pub rotation: RowMajorRotation,
75
+ /// Positions in nm.
76
+ pub positions: Vec<Position>,
77
+ }
78
+
79
+ impl Batch {
80
+ /// Create a new [`Batch`] from a rotation and a set of positions.
81
+ ///
82
+ /// - The locations of the `positions` must be provided in nm. Any resolution adjustments
83
+ /// must be applied by the caller.
84
+ /// - The provided `rotation` is internally converted and stored in row-major order.
85
+ pub fn new(rotation: Rotation, positions: Vec<Position>) -> Self {
86
+ Self {
87
+ rotation: rotation.transpose().to_cols_array_2d(),
88
+ positions,
89
+ }
90
+ }
91
+
92
+ /// Returns the 3×3 rotation matrix of this [`Batch`].
93
+ ///
94
+ /// The rotation matrix is returned in column-major order.
95
+ pub fn rotation(&self) -> Mat3 {
96
+ // Because the batch rotation is stored in a row-major order, and the initializer we use
97
+ // here assumes column-major order, we need to transpose the matrix before using it.
98
+ Mat3::from_cols_array_2d(&self.rotation).transpose()
99
+ }
100
+ }
core/utilities.rs ADDED
@@ -0,0 +1 @@
1
+ pub const CLEAR_LINE: &str = "\u{1b}[2K\r";
core/version.rs ADDED
@@ -0,0 +1,32 @@
1
+ //! Provide additional version information, including the current git hash.
2
+
3
+ pub struct Version {
4
+ pkg_version: &'static str,
5
+ git_version: &'static str,
6
+ }
7
+
8
+ impl std::fmt::Display for Version {
9
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
10
+ let Self {
11
+ pkg_version,
12
+ git_version,
13
+ } = self;
14
+ write!(f, "{pkg_version} ({git_version})")
15
+ }
16
+ }
17
+
18
+ // This makes it possible to use it as the version that Clap displays for a command.
19
+ impl From<Version> for clap::builder::Str {
20
+ fn from(version: Version) -> Self {
21
+ version.to_string().into()
22
+ }
23
+ }
24
+
25
+ pub const VERSION: Version = Version {
26
+ pkg_version: env!("CARGO_PKG_VERSION"),
27
+ git_version: git_version::git_version!(
28
+ args = ["--broken", "--always", "--exclude", "*"],
29
+ prefix = "git:",
30
+ fallback = "release"
31
+ ),
32
+ };
init/example.bent ADDED
@@ -0,0 +1,74 @@
1
+ # This is an example of a bentopy input file.
2
+ # Change the <placeholders> to the values relevant for your system.
3
+ # Some advanced options exist for the general and space sections that are not
4
+ # listed here. You can find more information about these in our documentation.
5
+
6
+ [ general ]
7
+ title <system-description>
8
+ seed <seed>
9
+
10
+ [ space ]
11
+ # Points or dimensions are given as comma-separated tuples of three numbers.
12
+ # Whitespace between the commas and the numbers is optional.
13
+ dimensions <x>, <y>, <z>
14
+ # All dimensions and sizes in bentopy are given in nanometers.
15
+ resolution <voxel-size>
16
+
17
+ [ includes ]
18
+ # You can list the itp files that should be included in the topology files.
19
+ # This is optional. For example:
20
+ # "forcefield/forcefield.itp"
21
+ # "forcefield/forcefield_solvents.itp"
22
+ # "structures/*.itp"
23
+ <itp-includes...>
24
+
25
+ [ compartments ]
26
+ # At least one compartment must be defined.
27
+ # You can select the whole space.
28
+ <id> is all
29
+ # Load from existing voxel mask (npz).
30
+ <id> from <voxel-mask-path>
31
+ # Or create them using analytical functions.
32
+ <id> as sphere at center with radius <radius>
33
+ <id> as sphere at <x>, <y>, <z> with radius <radius>
34
+ <id> as cuboid from <x>, <y>, <z> to <x>, <y>, <z>
35
+ # Like the center point anchor, you can also use start and end anchors.
36
+ <id> as cuboid from start to end
37
+ # Placements can be limited to geometric limits. These can be phrased as a
38
+ # single inequality, such as:
39
+ # x > 40
40
+ # or using boolean expressions (just like compartment combinations):
41
+ # 20 < x and x < 80 and not z > 20
42
+ <id> where <limits-expression>
43
+ # Placements can be further restricted to a certain distance from some compartment.
44
+ <id> within 5.0 of <compartment-id>
45
+ # Compartments can be combined.
46
+ # The available operators are `and`, `or`, and `not`.
47
+ # Expressions can be grouped using parentheses. For example:
48
+ # not a or (b and c)
49
+ <id> combines <combination-expression>
50
+
51
+ [ constraints ]
52
+ # The rotational axes of a placement can be restricted. Axes are provided as a
53
+ # comma-separated list. Random rotations are only applied to the listed axes.
54
+ # The rotation over the axes that are not listed will be the same as in the
55
+ # original structure file for a segment.
56
+ <rule-id> rotates <axes>
57
+
58
+ [ segments ]
59
+ # On each line, a segment is defined as follows:
60
+ # - Name (match naming in itp files to write a correct topology file).
61
+ # - Optionally, a tag (max 5 characters, preceded by a colon).
62
+ # - Quantity:
63
+ # - concentration (number, with M or mM, uM/µM, nM, pM suffix),
64
+ # - copy number (integer, no suffix).
65
+ # - Path to the structure file (gro, pdb).
66
+ # - Names of its compartments (comma-separated).
67
+ # - Optionally, the names of the rules it should uphold (comma-separated).
68
+ <name>:<tag> <quantity> from <structure-path> in <compartment-ids> satisfies <rules>
69
+ # If desired, newlines (and other whitespace) between fields is allowed, making
70
+ # it possible to write segment declarations like this example:
71
+ # 3lyz:inside
72
+ # 1uM
73
+ # from "structures/3lyz.pdb"
74
+ # in sphere