1use std::collections::BTreeSet;
4use std::error::Error;
5use std::fmt;
6
7use ndarray::array;
8
9use crate::spatial::simbox::SimBox;
10use crate::store::frame::Frame;
11use crate::store::keys;
12use crate::system::atomistic::Atomistic;
13use crate::types::F;
14
15#[derive(Debug, Clone, PartialEq)]
17pub enum GrapheneError {
18 InvalidSize,
20 InvalidParameter(&'static str),
22 Graph(String),
24 Cell(String),
26}
27
28impl fmt::Display for GrapheneError {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Self::InvalidSize => {
32 write!(f, "nx and ny must be positive and not larger than 10_000")
33 }
34 Self::InvalidParameter(name) => write!(f, "{name} is invalid"),
35 Self::Graph(message) => write!(f, "could not build graphene graph: {message}"),
36 Self::Cell(message) => write!(f, "could not build graphene cell: {message}"),
37 }
38 }
39}
40
41impl Error for GrapheneError {}
42
43#[derive(Debug, Clone)]
55pub struct GrapheneBuilder {
56 nx: u32,
57 ny: u32,
58 bond_length: f64,
59 vacuum: f64,
60 periodic_xy: bool,
61 atom_type: Option<String>,
62 charge: f64,
63}
64
65impl GrapheneBuilder {
66 pub fn new(nx: u32, ny: u32) -> Result<Self, GrapheneError> {
68 if nx == 0 || ny == 0 || nx > 10_000 || ny > 10_000 {
69 return Err(GrapheneError::InvalidSize);
70 }
71 Ok(Self {
72 nx,
73 ny,
74 bond_length: 1.42,
75 vacuum: 10.0,
76 periodic_xy: true,
77 atom_type: None,
78 charge: 0.0,
79 })
80 }
81
82 pub fn with_bond_length(mut self, bond_length: f64) -> Result<Self, GrapheneError> {
84 if !bond_length.is_finite() || bond_length <= 0.0 {
85 return Err(GrapheneError::InvalidParameter("bond_length"));
86 }
87 self.bond_length = bond_length;
88 Ok(self)
89 }
90
91 pub fn with_vacuum(mut self, vacuum: f64) -> Result<Self, GrapheneError> {
93 if !vacuum.is_finite() || vacuum < 0.0 {
94 return Err(GrapheneError::InvalidParameter("vacuum"));
95 }
96 self.vacuum = vacuum;
97 Ok(self)
98 }
99
100 pub fn with_periodic_xy(mut self, periodic_xy: bool) -> Self {
102 self.periodic_xy = periodic_xy;
103 self
104 }
105
106 pub fn with_atom_type(mut self, atom_type: impl Into<String>) -> Result<Self, GrapheneError> {
108 let atom_type = atom_type.into();
109 if atom_type.is_empty() {
110 return Err(GrapheneError::InvalidParameter("atom_type"));
111 }
112 self.atom_type = Some(atom_type);
113 Ok(self)
114 }
115
116 pub fn with_charge(mut self, charge: f64) -> Result<Self, GrapheneError> {
118 if !charge.is_finite() {
119 return Err(GrapheneError::InvalidParameter("charge"));
120 }
121 self.charge = charge;
122 Ok(self)
123 }
124
125 pub fn nx(&self) -> u32 {
127 self.nx
128 }
129
130 pub fn ny(&self) -> u32 {
132 self.ny
133 }
134
135 pub fn bond_length(&self) -> f64 {
137 self.bond_length
138 }
139
140 pub fn periodic_xy(&self) -> bool {
142 self.periodic_xy
143 }
144
145 pub fn build(&self) -> Result<Frame, GrapheneError> {
147 let a = self.bond_length;
148 let a1x = 3.0_f64.sqrt() * a;
149 let a2x = 0.5 * a1x;
150 let a2y = 1.5 * a;
151 let nx = self.nx as usize;
152 let ny = self.ny as usize;
153
154 let idx = |i: usize, j: usize, s: usize| -> usize { 2 * (i + j * nx) + s };
155 let wrap = |i: isize, n: usize| -> Option<usize> {
156 if self.periodic_xy {
157 Some(i.rem_euclid(n as isize) as usize)
158 } else if (0..n as isize).contains(&i) {
159 Some(i as usize)
160 } else {
161 None
162 }
163 };
164
165 let mut graph = Atomistic::new();
166 let mut atoms = Vec::with_capacity(2 * nx * ny);
167 for j in 0..ny {
168 for i in 0..nx {
169 let ox = i as f64 * a1x + j as f64 * a2x;
170 let oy = j as f64 * a2y;
171 for (dx, dy) in [(0.0, 0.0), (a1x / 3.0 + a2x / 3.0, a2y / 3.0)] {
172 let atom = graph.add_atom_xyz("C", ox + dx, oy + dy, 0.0);
173 graph
174 .set_atom(atom, keys::CHARGE, self.charge)
175 .map_err(|e| GrapheneError::Graph(e.to_string()))?;
176 if let Some(ref t) = self.atom_type {
177 graph
178 .set_atom(atom, keys::TYPE, t.as_str())
179 .map_err(|e| GrapheneError::Graph(e.to_string()))?;
180 }
181 atoms.push(atom);
182 }
183 }
184 }
185
186 let mut bonds = BTreeSet::new();
188 for j in 0..ny {
189 for i in 0..nx {
190 let a0 = idx(i, j, 0);
191 let partners = [
192 Some(idx(i, j, 1)),
193 wrap(i as isize - 1, nx).map(|ii| idx(ii, j, 1)),
194 wrap(j as isize - 1, ny).map(|jj| idx(i, jj, 1)),
195 ];
196 for partner in partners.into_iter().flatten() {
197 bonds.insert(if a0 < partner {
198 (a0, partner)
199 } else {
200 (partner, a0)
201 });
202 }
203 }
204 }
205 for (u, v) in bonds {
206 graph
207 .add_bond(atoms[u], atoms[v])
208 .map_err(|e| GrapheneError::Graph(e.to_string()))?;
209 }
210
211 let mut frame = graph.to_frame();
212 frame.simbox = Some(self.cell()?);
213 Ok(frame)
214 }
215
216 pub fn cell(&self) -> Result<SimBox, GrapheneError> {
218 let a = self.bond_length;
219 let a1x = 3.0_f64.sqrt() * a;
220 let a2y = 1.5 * a;
221 let lx = self.nx as f64 * a1x;
222 let ly = self.ny as f64 * a2y;
223 let lz = self.vacuum.max(a);
224 SimBox::ortho(
225 array![lx as F, ly as F, lz as F],
226 array![0.0 as F, 0.0 as F, -lz * 0.5 as F],
227 [self.periodic_xy, self.periodic_xy, false],
228 )
229 .map_err(|e| GrapheneError::Cell(format!("{e:?}")))
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn small_periodic_sheet_has_three_bonds_per_atom() {
239 let frame = GrapheneBuilder::new(4, 4)
240 .unwrap()
241 .with_periodic_xy(true)
242 .build()
243 .unwrap();
244 let n = frame.get("atoms").unwrap().nrows().unwrap();
245 assert_eq!(n, 32);
246 let bonds = frame.get("bonds").unwrap();
247 assert_eq!(bonds.nrows(), Some(3 * n / 2));
248
249 let mut degree = vec![0; n];
250 for &i in bonds.get_uint("atomi").unwrap() {
251 degree[i as usize] += 1;
252 }
253 for &j in bonds.get_uint("atomj").unwrap() {
254 degree[j as usize] += 1;
255 }
256 assert!(degree.into_iter().all(|d| d == 3));
257 }
258
259 #[test]
260 fn open_sheet_has_fewer_bonds_than_periodic() {
261 let open = GrapheneBuilder::new(3, 3)
262 .unwrap()
263 .with_periodic_xy(false)
264 .build()
265 .unwrap();
266 let closed = GrapheneBuilder::new(3, 3)
267 .unwrap()
268 .with_periodic_xy(true)
269 .build()
270 .unwrap();
271 assert!(
272 open.get("bonds").unwrap().nrows().unwrap()
273 < closed.get("bonds").unwrap().nrows().unwrap()
274 );
275 }
276}