use surface::{Sheet, LatticeType};
use coord::{Coord, Direction, Translate,
rotate_coords, rotate_planar_coords_to_alignment};
use describe::{unwrap_name, Describe};
use error::Result;
use iterator::{AtomIterator, AtomIterItem};
use system::*;
use std::f64::consts::PI;
use std::fmt;
use std::fmt::{Display, Formatter};
impl_component![Cylinder];
impl_translate![Cylinder];
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
pub enum CylinderCap {
Top,
Bottom,
Both,
}
impl Display for CylinderCap {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
CylinderCap::Top => write!(f, "Top"),
CylinderCap::Bottom => write!(f, "Bottom"),
CylinderCap::Both => write!(f, "Both"),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Cylinder {
pub name: Option<String>,
pub residue: Option<Residue>,
pub lattice: LatticeType,
pub alignment: Direction,
pub cap: Option<CylinderCap>,
#[serde(skip)]
pub origin: Coord,
#[serde(skip)]
pub radius: f64,
#[serde(skip)]
pub height: f64,
#[serde(skip)]
pub coords: Vec<Coord>,
}
impl Cylinder {
pub fn construct(self) -> Result<Cylinder> {
let length = 2.0 * PI * self.radius;
let width = self.height;
let sheet = Sheet {
name: None,
residue: None,
lattice: self.lattice.clone(),
std_z: None,
origin: Coord::default(),
normal: Direction::Z,
length,
width,
coords: vec![],
}.construct()?;
let final_radius = sheet.length / (2.0 * PI);
let final_height = sheet.width;
let mut coords: Vec<_> = sheet.coords
.iter()
.map(|coord| {
let (x0, y, _) = coord.to_tuple();
let angle = (x0 * 360.0 / sheet.length).to_radians();
let x = final_radius * angle.sin();
let z = -final_radius * angle.cos();
Coord::new(x, y, z)
})
.collect();
if let Some(cap) = self.cap {
let mut bottom = sheet.to_circle(final_radius); bottom.coords = rotate_planar_coords_to_alignment(&bottom.coords,
Direction::Z, Direction::Y);
let top_coords: Vec<_> = bottom.coords
.iter()
.map(|&coord| coord + Coord::new(0.0, final_height, 0.0))
.collect();
match cap {
CylinderCap::Bottom => coords.extend_from_slice(&bottom.coords),
CylinderCap::Top => coords.extend_from_slice(&top_coords),
CylinderCap::Both => {
coords.extend_from_slice(&bottom.coords);
coords.extend_from_slice(&top_coords);
}
}
}
Ok(Cylinder {
alignment: Direction::Z,
radius: final_radius,
height: final_height,
coords: rotate_coords(&coords, Direction::X),
.. self
})
}
fn calc_box_size(&self) -> Coord {
let diameter = 2.0 * self.radius;
match self.alignment {
Direction::X => Coord::new(self.height, diameter, diameter),
Direction::Y => Coord::new(diameter, self.height, diameter),
Direction::Z => Coord::new(diameter, diameter, self.height),
}
}
}
impl Describe for Cylinder {
fn describe(&self) -> String {
format!("{} (Cylinder surface of radius {:.2} and height {:.2} at {})",
unwrap_name(&self.name), self.radius, self.height, self.origin)
}
fn describe_short(&self) -> String {
format!("{} (Cylinder)", unwrap_name(&self.name))
}
}
#[cfg(test)]
mod tests {
use super::*;
use surface::LatticeType::*;
fn setup_cylinder(radius: f64, height: f64, lattice: &LatticeType) -> Cylinder {
Cylinder {
name: None,
residue: None,
lattice: lattice.clone(),
alignment: Direction::Z,
cap: None,
origin: Coord::default(),
radius,
height,
coords: vec![],
}
}
#[test]
fn cylinder_is_bent_from_sheet_as_expected() {
let radius = 2.0;
let height = 5.0;
let density = 10.0;
let lattice = PoissonDisc { density };
let cylinder = setup_cylinder(radius, height, &lattice).construct().unwrap();
let expected = 2.0 * PI * radius * height * density;
assert!((expected - cylinder.coords.len() as f64).abs() / expected < 0.1);
let sum_z = cylinder.coords.iter().map(|&Coord { x: _, y: _, z }| z.abs()).sum::<f64>();
assert!(sum_z > 0.0);
assert_eq!(Direction::Z, cylinder.alignment);
for coord in cylinder.coords {
let (r, h) = Coord::ORIGO.distance_cylindrical(coord, Direction::Z);
assert!(r <= cylinder.radius);
assert!(h >= 0.0 && h <= cylinder.height);
}
}
#[test]
fn cylinder_corrects_radius_and_height_to_match_lattice_spacing() {
let radius = 1.0; let height = 5.0;
let a = 1.0; let b = 1.1; let lattice = Triclinic { a, b, gamma: 90.0 };
let cylinder = setup_cylinder(radius, height, &lattice).construct().unwrap();
assert_ne!(radius, cylinder.radius);
assert_ne!(height, cylinder.height);
assert_eq!(6.0 * a / (2.0 * PI), cylinder.radius);
assert_eq!(5.0 * b, cylinder.height);
}
#[test]
fn constructing_cylinder_with_negative_radius_or_height_returns_error() {
let lattice = PoissonDisc { density: 10.0 };
assert!(setup_cylinder(-1.0, 1.0, &lattice).construct().is_err());
assert!(setup_cylinder(1.0, -1.0, &lattice).construct().is_err());
assert!(setup_cylinder(1.0, 1.0, &lattice).construct().is_ok());
}
#[test]
fn add_caps_to_cylinder() {
let radius = 2.0;
let height = 5.0;
let lattice = Hexagonal { a: 0.1 };
let mut conf = setup_cylinder(radius, height, &lattice);
let cylinder = conf.clone().construct().unwrap();
let num_coords = cylinder.coords.len();
conf.cap = Some(CylinderCap::Bottom);
let cylinder_cap = conf.clone().construct().unwrap();
let (original, bottom) = cylinder_cap.coords.split_at(num_coords);
assert_eq!(&original, &cylinder.coords.as_slice());
assert!(bottom.len() > 0);
for coord in bottom {
assert_eq!(coord.z, 0.0);
}
conf.cap = Some(CylinderCap::Top);
let cylinder_cap = conf.clone().construct().unwrap();
let (original, top) = cylinder_cap.coords.split_at(num_coords);
assert_eq!(&original, &cylinder.coords.as_slice());
assert_eq!(top.len(), bottom.len());
for coord in top {
assert_eq!(coord.z, cylinder.height);
}
conf.cap = Some(CylinderCap::Both);
let cylinder_cap = conf.clone().construct().unwrap();
let (original, bottom_and_top) = cylinder_cap.coords.split_at(num_coords);
assert_eq!(&original, &cylinder.coords.as_slice());
let (bottom_from_both, top_from_both) = bottom_and_top.split_at(bottom.len());
assert_eq!(bottom, bottom_from_both);
assert_eq!(top, top_from_both);
}
#[test]
fn calc_box_size_of_cylinder() {
let radius = 2.0;
let height = 5.0;
let lattice = Hexagonal { a: 0.1 };
let mut cylinder = Cylinder {
alignment: Direction::X,
.. setup_cylinder(radius, height, &lattice)
};
let diameter = 2.0 * radius;
assert_eq!(Coord::new(height, diameter, diameter), cylinder.calc_box_size());
cylinder.alignment = Direction::Y;
assert_eq!(Coord::new(diameter, height, diameter), cylinder.calc_box_size());
cylinder.alignment = Direction::Z;
assert_eq!(Coord::new(diameter, diameter, height), cylinder.calc_box_size());
}
}