Skip to main content

molrs/builder/
graphene.rs

1//! Flat graphene (honeycomb) sheet builder.
2
3use 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/// Error returned when graphene sheet parameters are invalid.
16#[derive(Debug, Clone, PartialEq)]
17pub enum GrapheneError {
18    /// A dimension is zero or impractically large.
19    InvalidSize,
20    /// A scalar parameter is non-finite or outside its allowed range.
21    InvalidParameter(&'static str),
22    /// A bond or atom property could not be written.
23    Graph(String),
24    /// The simulation cell could not be built.
25    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/// Builder for a rectangular graphene sheet [`Frame`].
44///
45/// Honeycomb lattice with bond length `a` and lattice vectors
46///
47/// ```text
48/// a₁ = (√3 a, 0)
49/// a₂ = (√3 a / 2, 3 a / 2)
50/// ```
51///
52/// Two carbon sites (A, B) per unit cell. `nx × ny` cells → `2·nx·ny` atoms.
53/// In-plane bonds wrap when [`with_periodic_xy`](Self::with_periodic_xy) is true.
54#[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    /// Start a sheet builder for `nx × ny` honeycomb unit cells.
67    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    /// Carbon–carbon bond length in ångström (default 1.42).
83    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    /// Vacuum padding along *z* (default 10 Å).
92    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    /// Close bonds across the *xy* periodic images (default `true`).
101    pub fn with_periodic_xy(mut self, periodic_xy: bool) -> Self {
102        self.periodic_xy = periodic_xy;
103        self
104    }
105
106    /// Optional force-field atom type for every carbon.
107    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    /// Finite partial charge on every carbon.
117    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    /// Number of unit cells along **a₁**.
126    pub fn nx(&self) -> u32 {
127        self.nx
128    }
129
130    /// Number of unit cells along **a₂**.
131    pub fn ny(&self) -> u32 {
132        self.ny
133    }
134
135    /// Carbon–carbon bond length in ångström.
136    pub fn bond_length(&self) -> f64 {
137        self.bond_length
138    }
139
140    /// Whether *xy* bonds wrap across the cell.
141    pub fn periodic_xy(&self) -> bool {
142        self.periodic_xy
143    }
144
145    /// Build a fresh molecular [`Frame`] (atoms, bonds, orthorhombic box).
146    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        // A (s=0) bonds to three B (s=1) sites: (i,j), (i-1,j), (i,j-1).
187        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    /// Simulation cell matching the generated sheet.
217    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}