1use std::path::PathBuf;
12
13use super::error::ScriptError;
14
15#[derive(Debug, Clone)]
21pub struct Script {
22 pub tolerance: f64,
24 pub seed: Option<u64>,
26 pub filetype: Option<String>,
30 pub output: PathBuf,
32 pub nloop: usize,
35 pub avoid_overlap: bool,
39 pub pbc: Option<PbcSpec>,
43 pub structures: Vec<Structure>,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq)]
54pub struct PbcSpec {
55 pub min: [f64; 3],
56 pub max: [f64; 3],
57}
58
59#[derive(Debug, Clone)]
61pub struct Structure {
62 pub filepath: PathBuf,
64 pub number: usize,
66 pub mol_restraints: Vec<RestraintSpec>,
68 pub atom_groups: Vec<AtomGroup>,
70 pub center: bool,
72 pub fixed: Option<([f64; 3], [f64; 3])>,
74}
75
76#[derive(Debug, Clone)]
78pub struct AtomGroup {
79 pub atom_indices: Vec<usize>,
81 pub restraints: Vec<RestraintSpec>,
82}
83
84#[derive(Debug, Clone)]
87pub enum RestraintSpec {
88 InsideBox {
89 min: [f64; 3],
90 max: [f64; 3],
91 },
92 InsideSphere {
93 center: [f64; 3],
94 radius: f64,
95 },
96 OutsideSphere {
97 center: [f64; 3],
98 radius: f64,
99 },
100 AbovePlane {
102 normal: [f64; 3],
103 distance: f64,
104 },
105 BelowPlane {
107 normal: [f64; 3],
108 distance: f64,
109 },
110 InsideCube {
112 origin: [f64; 3],
113 side: f64,
114 },
115 OutsideCube {
117 origin: [f64; 3],
118 side: f64,
119 },
120 OutsideBox {
122 min: [f64; 3],
123 max: [f64; 3],
124 },
125 InsideEllipsoid {
127 center: [f64; 3],
128 axes: [f64; 3],
129 exponent: f64,
130 },
131 OutsideEllipsoid {
133 center: [f64; 3],
134 axes: [f64; 3],
135 exponent: f64,
136 },
137 InsideCylinder {
139 center: [f64; 3],
140 axis: [f64; 3],
141 radius: f64,
142 length: f64,
143 },
144 OutsideCylinder {
146 center: [f64; 3],
147 axis: [f64; 3],
148 radius: f64,
149 length: f64,
150 },
151}
152
153pub fn parse(src: &str) -> Result<Script, ScriptError> {
159 let mut tolerance = 2.0_f64;
160 let mut seed: Option<u64> = None;
161 let mut filetype: Option<String> = None;
162 let mut output: Option<PathBuf> = None;
163 let mut nloop: Option<usize> = None;
164 let mut avoid_overlap = true;
165 let mut pbc: Option<PbcSpec> = None;
166 let mut structures: Vec<Structure> = Vec::new();
167
168 enum State {
169 TopLevel,
170 InStructure(Structure),
171 InAtoms {
172 structure: Structure,
173 group: AtomGroup,
174 },
175 }
176
177 let mut state = State::TopLevel;
178
179 for (lineno, raw) in src.lines().enumerate() {
180 let lineno = lineno + 1;
181
182 let line = match raw.find('#') {
184 Some(pos) => &raw[..pos],
185 None => raw,
186 }
187 .trim();
188
189 if line.is_empty() {
190 continue;
191 }
192
193 let tokens: Vec<&str> = line.split_whitespace().collect();
194 let keyword = tokens[0].to_ascii_lowercase();
195
196 state = match state {
197 State::TopLevel => match keyword.as_str() {
199 "tolerance" => {
200 tolerance = parse_f64(&tokens, 1, "tolerance", lineno)?;
201 State::TopLevel
202 }
203 "seed" => {
204 seed = Some(parse_u64(&tokens, 1, "seed", lineno)?);
205 State::TopLevel
206 }
207 "filetype" => {
208 filetype = Some(
209 tokens
210 .get(1)
211 .ok_or_else(|| parse_err(lineno, "missing filetype value"))?
212 .to_ascii_lowercase(),
213 );
214 State::TopLevel
215 }
216 "output" => {
217 output = Some(PathBuf::from(
218 tokens
219 .get(1)
220 .ok_or_else(|| parse_err(lineno, "missing output path"))?,
221 ));
222 State::TopLevel
223 }
224 "nloop" => {
225 nloop = Some(parse_usize(&tokens, 1, "nloop", lineno)?);
226 State::TopLevel
227 }
228 "avoid_overlap" => {
229 let val = tokens
230 .get(1)
231 .map(|s| s.to_ascii_lowercase())
232 .unwrap_or_default();
233 avoid_overlap = val != "no" && val != "false" && val != "0";
234 State::TopLevel
235 }
236 "pbc" => {
237 pbc = Some(parse_pbc(&tokens, lineno)?);
238 State::TopLevel
239 }
240 "structure" => {
241 let path = tokens
242 .get(1)
243 .ok_or_else(|| parse_err(lineno, "`structure` requires a file path"))?;
244 State::InStructure(Structure {
245 filepath: PathBuf::from(path),
246 number: 0,
247 mol_restraints: Vec::new(),
248 atom_groups: Vec::new(),
249 center: false,
250 fixed: None,
251 })
252 }
253 _ => {
254 return Err(unknown_keyword(lineno, &keyword, "top-level"));
255 }
256 },
257
258 State::InStructure(mut s) => match keyword.as_str() {
260 "end"
261 if tokens.get(1).map(|t| t.to_ascii_lowercase()).as_deref()
262 == Some("structure") =>
263 {
264 structures.push(s);
265 State::TopLevel
266 }
267 "number" => {
268 s.number = parse_usize(&tokens, 1, "number", lineno)?;
269 State::InStructure(s)
270 }
271 "center" | "centerofmass" => {
272 s.center = true;
273 State::InStructure(s)
274 }
275 "fixed" => {
276 let pos = parse_vec3(&tokens, 1, "fixed position", lineno)?;
277 let euler = parse_vec3(&tokens, 4, "fixed euler", lineno)?;
278 s.fixed = Some((pos, euler));
279 State::InStructure(s)
280 }
281 "inside" => {
282 let r = parse_inside(&tokens, lineno)?;
283 s.mol_restraints.push(r);
284 State::InStructure(s)
285 }
286 "outside" => {
287 let r = parse_outside(&tokens, lineno)?;
288 s.mol_restraints.push(r);
289 State::InStructure(s)
290 }
291 "over" | "above" => {
292 let r = parse_plane_above(&tokens, lineno)?;
293 s.mol_restraints.push(r);
294 State::InStructure(s)
295 }
296 "below" => {
297 let r = parse_plane_below(&tokens, lineno)?;
298 s.mol_restraints.push(r);
299 State::InStructure(s)
300 }
301 "atoms" => {
302 let indices = tokens[1..]
303 .iter()
304 .map(|t| {
305 t.parse::<usize>()
306 .map_err(|_| parse_err(lineno, format!("invalid atom index `{t}`")))
307 })
308 .collect::<Result<Vec<_>, _>>()?;
309 if indices.is_empty() {
310 return Err(parse_err(lineno, "`atoms` requires at least one index"));
311 }
312 State::InAtoms {
313 structure: s,
314 group: AtomGroup {
315 atom_indices: indices,
316 restraints: Vec::new(),
317 },
318 }
319 }
320 _ => {
321 return Err(unknown_keyword(lineno, &keyword, "structure block"));
322 }
323 },
324
325 State::InAtoms {
327 structure: s,
328 mut group,
329 } => match keyword.as_str() {
330 "end"
331 if tokens.get(1).map(|t| t.to_ascii_lowercase()).as_deref()
332 == Some("atoms") =>
333 {
334 let mut s = s;
335 s.atom_groups.push(group);
336 State::InStructure(s)
337 }
338 "inside" => {
339 let r = parse_inside(&tokens, lineno)?;
340 group.restraints.push(r);
341 State::InAtoms {
342 structure: s,
343 group,
344 }
345 }
346 "outside" => {
347 let r = parse_outside(&tokens, lineno)?;
348 group.restraints.push(r);
349 State::InAtoms {
350 structure: s,
351 group,
352 }
353 }
354 "over" | "above" => {
355 let r = parse_plane_above(&tokens, lineno)?;
356 group.restraints.push(r);
357 State::InAtoms {
358 structure: s,
359 group,
360 }
361 }
362 "below" => {
363 let r = parse_plane_below(&tokens, lineno)?;
364 group.restraints.push(r);
365 State::InAtoms {
366 structure: s,
367 group,
368 }
369 }
370 _ => {
371 return Err(unknown_keyword(lineno, &keyword, "atoms block"));
372 }
373 },
374 };
375 }
376
377 match state {
378 State::InStructure(_) => return Err(ScriptError::UnclosedBlock("structure")),
379 State::InAtoms { .. } => return Err(ScriptError::UnclosedBlock("atoms")),
380 State::TopLevel => {}
381 }
382
383 Ok(Script {
384 tolerance,
385 seed,
386 filetype,
387 output: output.ok_or(ScriptError::MissingOutput)?,
388 nloop: nloop.unwrap_or(200 * structures.len()),
391 avoid_overlap,
392 pbc,
393 structures,
394 })
395}
396
397fn parse_inside(tokens: &[&str], lineno: usize) -> Result<RestraintSpec, ScriptError> {
402 let shape = tokens
403 .get(1)
404 .map(|s| s.to_ascii_lowercase())
405 .ok_or_else(|| parse_err(lineno, "`inside` requires a shape keyword"))?;
406 match shape.as_str() {
407 "box" => {
408 let min = parse_vec3(tokens, 2, "inside box min", lineno)?;
409 let max = parse_vec3(tokens, 5, "inside box max", lineno)?;
410 Ok(RestraintSpec::InsideBox { min, max })
411 }
412 "cube" => {
413 let (origin, side) = parse_cube_args(tokens, lineno, "inside cube")?;
414 Ok(RestraintSpec::InsideCube { origin, side })
415 }
416 "sphere" => {
417 let center = parse_vec3(tokens, 2, "inside sphere center", lineno)?;
418 let radius = parse_f64(tokens, 5, "inside sphere radius", lineno)?;
419 Ok(RestraintSpec::InsideSphere { center, radius })
420 }
421 "ellipsoid" => {
422 let (center, axes, exponent) =
423 parse_ellipsoid_args(tokens, lineno, "inside ellipsoid")?;
424 Ok(RestraintSpec::InsideEllipsoid {
425 center,
426 axes,
427 exponent,
428 })
429 }
430 "cylinder" => {
431 let (center, axis, radius, length) =
432 parse_cylinder_args(tokens, lineno, "inside cylinder")?;
433 Ok(RestraintSpec::InsideCylinder {
434 center,
435 axis,
436 radius,
437 length,
438 })
439 }
440 _ => Err(parse_err(
441 lineno,
442 format!("unsupported `inside` shape `{shape}`"),
443 )),
444 }
445}
446
447fn parse_outside(tokens: &[&str], lineno: usize) -> Result<RestraintSpec, ScriptError> {
448 let shape = tokens
449 .get(1)
450 .map(|s| s.to_ascii_lowercase())
451 .ok_or_else(|| parse_err(lineno, "`outside` requires a shape keyword"))?;
452 match shape.as_str() {
453 "box" => {
454 let min = parse_vec3(tokens, 2, "outside box min", lineno)?;
455 let max = parse_vec3(tokens, 5, "outside box max", lineno)?;
456 Ok(RestraintSpec::OutsideBox { min, max })
457 }
458 "cube" => {
459 let (origin, side) = parse_cube_args(tokens, lineno, "outside cube")?;
460 Ok(RestraintSpec::OutsideCube { origin, side })
461 }
462 "sphere" => {
463 let center = parse_vec3(tokens, 2, "outside sphere center", lineno)?;
464 let radius = parse_f64(tokens, 5, "outside sphere radius", lineno)?;
465 Ok(RestraintSpec::OutsideSphere { center, radius })
466 }
467 "ellipsoid" => {
468 let (center, axes, exponent) =
469 parse_ellipsoid_args(tokens, lineno, "outside ellipsoid")?;
470 Ok(RestraintSpec::OutsideEllipsoid {
471 center,
472 axes,
473 exponent,
474 })
475 }
476 "cylinder" => {
477 let (center, axis, radius, length) =
478 parse_cylinder_args(tokens, lineno, "outside cylinder")?;
479 Ok(RestraintSpec::OutsideCylinder {
480 center,
481 axis,
482 radius,
483 length,
484 })
485 }
486 _ => Err(parse_err(
487 lineno,
488 format!("unsupported `outside` shape `{shape}`"),
489 )),
490 }
491}
492
493fn parse_cube_args(
495 tokens: &[&str],
496 lineno: usize,
497 ctx: &str,
498) -> Result<([f64; 3], f64), ScriptError> {
499 let origin = parse_vec3(tokens, 2, ctx, lineno)?;
500 let side = parse_f64(tokens, 5, ctx, lineno)?;
501 Ok((origin, side))
502}
503
504fn parse_ellipsoid_args(
506 tokens: &[&str],
507 lineno: usize,
508 ctx: &str,
509) -> Result<([f64; 3], [f64; 3], f64), ScriptError> {
510 let center = parse_vec3(tokens, 2, ctx, lineno)?;
511 let axes = parse_vec3(tokens, 5, ctx, lineno)?;
512 if axes.iter().any(|&a| a <= 0.0) {
513 return Err(parse_err(
514 lineno,
515 format!("{ctx} semi-axes must be strictly positive, got {axes:?}"),
516 ));
517 }
518 let exponent = parse_f64(tokens, 8, ctx, lineno)?;
519 Ok((center, axes, exponent))
520}
521
522fn parse_cylinder_args(
524 tokens: &[&str],
525 lineno: usize,
526 ctx: &str,
527) -> Result<([f64; 3], [f64; 3], f64, f64), ScriptError> {
528 let center = parse_vec3(tokens, 2, ctx, lineno)?;
529 let axis = parse_vec3(tokens, 5, ctx, lineno)?;
530 if axis == [0.0; 3] {
531 return Err(parse_err(
532 lineno,
533 format!("{ctx} axis must be a non-zero direction"),
534 ));
535 }
536 let radius = parse_f64(tokens, 8, ctx, lineno)?;
537 let length = parse_f64(tokens, 9, ctx, lineno)?;
538 Ok((center, axis, radius, length))
539}
540
541fn parse_plane_above(tokens: &[&str], lineno: usize) -> Result<RestraintSpec, ScriptError> {
542 let normal = parse_vec3(tokens, 2, "over plane normal", lineno)?;
543 let distance = parse_f64(tokens, 5, "over plane distance", lineno)?;
544 Ok(RestraintSpec::AbovePlane { normal, distance })
545}
546
547fn parse_plane_below(tokens: &[&str], lineno: usize) -> Result<RestraintSpec, ScriptError> {
548 let normal = parse_vec3(tokens, 2, "below plane normal", lineno)?;
549 let distance = parse_f64(tokens, 5, "below plane distance", lineno)?;
550 Ok(RestraintSpec::BelowPlane { normal, distance })
551}
552
553fn parse_pbc(tokens: &[&str], lineno: usize) -> Result<PbcSpec, ScriptError> {
557 match tokens.len() - 1 {
559 3 => {
560 let size = parse_vec3(tokens, 1, "pbc box size", lineno)?;
561 if size.iter().any(|&v| v <= 0.0) {
562 return Err(parse_err(
563 lineno,
564 format!("`pbc` box length must be positive on every axis; got {size:?}"),
565 ));
566 }
567 Ok(PbcSpec {
568 min: [0.0; 3],
569 max: size,
570 })
571 }
572 6 => {
573 let min = parse_vec3(tokens, 1, "pbc min", lineno)?;
574 let max = parse_vec3(tokens, 4, "pbc max", lineno)?;
575 for k in 0..3 {
576 if max[k] <= min[k] {
577 return Err(parse_err(
578 lineno,
579 format!(
580 "`pbc` max must exceed min on every axis; axis {k}: min={}, max={}",
581 min[k], max[k]
582 ),
583 ));
584 }
585 }
586 Ok(PbcSpec { min, max })
587 }
588 n => Err(parse_err(
589 lineno,
590 format!("`pbc` expects 3 or 6 numeric values, got {n}"),
591 )),
592 }
593}
594
595pub(super) fn parse_f64(
600 tokens: &[&str],
601 idx: usize,
602 ctx: &str,
603 lineno: usize,
604) -> Result<f64, ScriptError> {
605 let tok = tokens
606 .get(idx)
607 .ok_or_else(|| parse_err(lineno, format!("`{ctx}` — missing value at position {idx}")))?;
608 tok.parse::<f64>()
609 .map_err(|_| parse_err(lineno, format!("`{ctx}` — `{tok}` is not a valid number")))
610}
611
612fn parse_u64(tokens: &[&str], idx: usize, ctx: &str, lineno: usize) -> Result<u64, ScriptError> {
613 let tok = tokens
614 .get(idx)
615 .ok_or_else(|| parse_err(lineno, format!("`{ctx}` — missing value at position {idx}")))?;
616 tok.parse::<u64>()
617 .map_err(|_| parse_err(lineno, format!("`{ctx}` — `{tok}` is not a valid integer")))
618}
619
620fn parse_usize(
621 tokens: &[&str],
622 idx: usize,
623 ctx: &str,
624 lineno: usize,
625) -> Result<usize, ScriptError> {
626 let tok = tokens
627 .get(idx)
628 .ok_or_else(|| parse_err(lineno, format!("`{ctx}` — missing value at position {idx}")))?;
629 tok.parse::<usize>()
630 .map_err(|_| parse_err(lineno, format!("`{ctx}` — `{tok}` is not a valid integer")))
631}
632
633pub(super) fn parse_vec3(
634 tokens: &[&str],
635 start: usize,
636 ctx: &str,
637 lineno: usize,
638) -> Result<[f64; 3], ScriptError> {
639 Ok([
640 parse_f64(tokens, start, ctx, lineno)?,
641 parse_f64(tokens, start + 1, ctx, lineno)?,
642 parse_f64(tokens, start + 2, ctx, lineno)?,
643 ])
644}
645
646pub(super) fn parse_err(lineno: usize, message: impl Into<String>) -> ScriptError {
647 ScriptError::Parse {
648 line: lineno,
649 message: message.into(),
650 }
651}
652
653fn unknown_keyword(lineno: usize, keyword: &str, context: &'static str) -> ScriptError {
654 ScriptError::UnknownKeyword {
655 line: lineno,
656 keyword: keyword.to_string(),
657 context,
658 }
659}
660
661#[cfg(test)]
666mod tests {
667 use super::*;
668
669 #[test]
670 fn parse_mixture() {
671 let src = "\
672tolerance 2.0
673seed 1234567
674filetype pdb
675output mixture.xyz
676
677structure water.pdb
678 number 1000
679 inside box 0. 0. 0. 40. 40. 40.
680end structure
681
682structure urea.pdb
683 number 400
684 inside box 0. 0. 0. 40. 40. 40.
685end structure
686";
687 let inp = parse(src).expect("parse failed");
688 assert_eq!(inp.tolerance, 2.0);
689 assert_eq!(inp.seed, Some(1_234_567));
690 assert_eq!(inp.filetype.as_deref(), Some("pdb"));
691 assert_eq!(inp.output, PathBuf::from("mixture.xyz"));
692 assert_eq!(inp.structures.len(), 2);
693 assert_eq!(inp.structures[0].number, 1000);
694 assert_eq!(inp.structures[1].number, 400);
695 assert_eq!(inp.structures[0].mol_restraints.len(), 1);
696 }
697
698 #[test]
699 fn parse_bilayer_atoms_blocks() {
700 let src = "\
701tolerance 2.0
702seed 1234567
703filetype pdb
704output bilayer.xyz
705
706structure palmitoil.pdb
707 number 10
708 inside box 0. 0. 0. 40. 40. 14.
709 atoms 31 32
710 below plane 0. 0. 1. 2.
711 end atoms
712 atoms 1 2
713 over plane 0. 0. 1. 12.
714 end atoms
715end structure
716";
717 let inp = parse(src).expect("parse failed");
718 let s = &inp.structures[0];
719 assert_eq!(s.number, 10);
720 assert_eq!(s.mol_restraints.len(), 1);
721 assert_eq!(s.atom_groups.len(), 2);
722 assert_eq!(s.atom_groups[0].atom_indices, vec![31, 32]);
723 assert_eq!(s.atom_groups[1].atom_indices, vec![1, 2]);
724 }
725
726 #[test]
727 fn parse_interface_fixed_center() {
728 let src = "\
729tolerance 2.0
730seed 1234567
731filetype pdb
732output interface.xyz
733
734structure t3.pdb
735 number 1
736 center
737 fixed 0. 20. 20. 1.57 1.57 1.57
738end structure
739";
740 let inp = parse(src).expect("parse failed");
741 let s = &inp.structures[0];
742 assert!(s.center);
743 let (pos, euler) = s.fixed.unwrap();
744 assert_eq!(pos, [0.0, 20.0, 20.0]);
745 assert!((euler[0] - 1.57).abs() < 1e-9);
746 }
747
748 #[test]
749 fn parse_centerofmass_alias() {
750 let src = "\
753tolerance 2.0
754filetype xyz
755output interface.xyz
756
757structure t3.xyz
758 number 1
759 centerofmass
760 fixed 0. 20. 20. 1.57 1.57 1.57
761end structure
762";
763 let inp = parse(src).expect("parse failed");
764 assert!(inp.structures[0].center);
765 }
766
767 #[test]
768 fn parse_comments_and_blank_lines() {
769 let src = "\
770# global settings
771tolerance 2.0
772# seed comment
773seed 42
774output out.pdb
775
776# molecule block
777structure mol.pdb
778 number 10 # trailing comment
779 inside sphere 0. 0. 0. 20.
780end structure
781";
782 let inp = parse(src).expect("parse failed");
783 assert_eq!(inp.seed, Some(42));
784 let s = &inp.structures[0];
785 assert_eq!(s.number, 10);
786 assert!(matches!(
787 s.mol_restraints[0],
788 RestraintSpec::InsideSphere { .. }
789 ));
790 }
791
792 #[test]
793 fn parse_avoid_overlap_no() {
794 let src = "\
795tolerance 2.0
796output out.pdb
797avoid_overlap no
798
799structure mol.pdb
800 number 1
801 inside sphere 0. 0. 0. 50.
802end structure
803";
804 let inp = parse(src).expect("parse failed");
805 assert!(!inp.avoid_overlap);
806 }
807
808 #[test]
809 fn parse_missing_output_errors() {
810 let src = "\
811tolerance 2.0
812
813structure mol.pdb
814 number 1
815 inside sphere 0. 0. 0. 50.
816end structure
817";
818 let err = parse(src).expect_err("should fail");
819 assert!(matches!(err, ScriptError::MissingOutput));
820 }
821
822 #[test]
823 fn parse_unclosed_structure_errors() {
824 let src = "\
825output out.pdb
826
827structure mol.pdb
828 number 1
829 inside sphere 0. 0. 0. 50.
830";
831 let err = parse(src).expect_err("should fail");
832 assert!(matches!(err, ScriptError::UnclosedBlock("structure")));
833 }
834
835 #[test]
836 fn parse_pbc_three_values() {
837 let src = "\
838tolerance 2.0
839output out.pdb
840pbc 100.0 80.0 60.0
841
842structure mol.pdb
843 number 1
844end structure
845";
846 let inp = parse(src).expect("parse failed");
847 let pbc = inp.pbc.expect("pbc should be set");
848 assert_eq!(pbc.min, [0.0, 0.0, 0.0]);
849 assert_eq!(pbc.max, [100.0, 80.0, 60.0]);
850 }
851
852 #[test]
853 fn parse_pbc_six_values() {
854 let src = "\
855tolerance 2.0
856output out.pdb
857pbc -50.0 -40.0 -30.0 50.0 40.0 30.0
858
859structure mol.pdb
860 number 1
861end structure
862";
863 let inp = parse(src).expect("parse failed");
864 let pbc = inp.pbc.expect("pbc should be set");
865 assert_eq!(pbc.min, [-50.0, -40.0, -30.0]);
866 assert_eq!(pbc.max, [50.0, 40.0, 30.0]);
867 }
868
869 #[test]
870 fn parse_pbc_rejects_bad_arity() {
871 let src = "\
872output out.pdb
873pbc 100.0 80.0
874
875structure mol.pdb
876 number 1
877end structure
878";
879 let err = parse(src).expect_err("should fail");
880 assert!(matches!(err, ScriptError::Parse { .. }));
881 }
882
883 #[test]
884 fn parse_pbc_rejects_non_positive_extent() {
885 let src = "\
886output out.pdb
887pbc 100.0 0.0 60.0
888
889structure mol.pdb
890 number 1
891end structure
892";
893 let err = parse(src).expect_err("should fail");
894 assert!(matches!(err, ScriptError::Parse { .. }));
895 }
896
897 #[test]
898 fn parse_unknown_top_level_keyword_errors() {
899 let src = "\
903output out.pdb
904wibble 1 2 3
905
906structure mol.pdb
907 number 1
908end structure
909";
910 let err = parse(src).expect_err("should fail");
911 match err {
912 ScriptError::UnknownKeyword {
913 keyword, context, ..
914 } => {
915 assert_eq!(keyword, "wibble");
916 assert_eq!(context, "top-level");
917 }
918 other => panic!("expected UnknownKeyword, got {other:?}"),
919 }
920 }
921
922 #[test]
923 fn parse_unknown_keyword_inside_structure_errors() {
924 let src = "\
925output out.pdb
926
927structure mol.pdb
928 number 1
929 nonsense foo
930end structure
931";
932 let err = parse(src).expect_err("should fail");
933 match err {
934 ScriptError::UnknownKeyword {
935 keyword, context, ..
936 } => {
937 assert_eq!(keyword, "nonsense");
938 assert_eq!(context, "structure block");
939 }
940 other => panic!("expected UnknownKeyword, got {other:?}"),
941 }
942 }
943
944 fn parse_one_restraint(line: &str) -> RestraintSpec {
947 let src =
948 format!("output out.pdb\n\nstructure mol.pdb\n number 1\n {line}\nend structure\n");
949 let inp = parse(&src).expect("parse failed");
950 inp.structures[0].mol_restraints[0].clone()
951 }
952
953 #[test]
954 fn parse_inside_cube() {
955 assert!(matches!(
956 parse_one_restraint("inside cube 1. 2. 3. 10."),
957 RestraintSpec::InsideCube { origin, side }
958 if origin == [1.0, 2.0, 3.0] && side == 10.0
959 ));
960 }
961
962 #[test]
963 fn parse_outside_cube() {
964 assert!(matches!(
965 parse_one_restraint("outside cube 0. 0. 0. 5."),
966 RestraintSpec::OutsideCube { side, .. } if side == 5.0
967 ));
968 }
969
970 #[test]
971 fn parse_outside_box() {
972 assert!(matches!(
973 parse_one_restraint("outside box 0. 0. 0. 4. 5. 6."),
974 RestraintSpec::OutsideBox { min, max }
975 if min == [0.0, 0.0, 0.0] && max == [4.0, 5.0, 6.0]
976 ));
977 }
978
979 #[test]
980 fn parse_inside_ellipsoid() {
981 assert!(matches!(
982 parse_one_restraint("inside ellipsoid 0. 0. 0. 3. 2. 1.5 1."),
983 RestraintSpec::InsideEllipsoid { axes, exponent, .. }
984 if axes == [3.0, 2.0, 1.5] && exponent == 1.0
985 ));
986 }
987
988 #[test]
989 fn parse_outside_ellipsoid() {
990 assert!(matches!(
991 parse_one_restraint("outside ellipsoid 1. 1. 1. 2. 2. 2. 1."),
992 RestraintSpec::OutsideEllipsoid { center, .. } if center == [1.0, 1.0, 1.0]
993 ));
994 }
995
996 #[test]
997 fn parse_inside_cylinder() {
998 assert!(matches!(
999 parse_one_restraint("inside cylinder 0. 0. 0. 1. 0. 0. 2. 8."),
1000 RestraintSpec::InsideCylinder { axis, radius, length, .. }
1001 if axis == [1.0, 0.0, 0.0] && radius == 2.0 && length == 8.0
1002 ));
1003 }
1004
1005 #[test]
1006 fn parse_outside_cylinder() {
1007 assert!(matches!(
1008 parse_one_restraint("outside cylinder 0. 0. 0. 0. 0. 1. 3. 5."),
1009 RestraintSpec::OutsideCylinder { axis, .. } if axis == [0.0, 0.0, 1.0]
1010 ));
1011 }
1012
1013 #[test]
1014 fn parse_ellipsoid_rejects_zero_axis() {
1015 let src = "output out.pdb\n\nstructure mol.pdb\n number 1\n \
1016 inside ellipsoid 0. 0. 0. 0. 2. 2. 1.\nend structure\n";
1017 assert!(parse(src).is_err(), "zero semi-axis must be rejected");
1018 }
1019
1020 #[test]
1021 fn parse_cylinder_rejects_zero_axis() {
1022 let src = "output out.pdb\n\nstructure mol.pdb\n number 1\n \
1023 inside cylinder 0. 0. 0. 0. 0. 0. 2. 8.\nend structure\n";
1024 assert!(parse(src).is_err(), "zero cylinder axis must be rejected");
1025 }
1026}