1#![allow(clippy::indexing_slicing, clippy::needless_range_loop)] use serde::{Deserialize, Serialize};
11
12use crate::polyhedron::{HalfSpace, Polyhedron};
13use crate::{Error, Lattice, Site, Structure, StructureBuilder};
14
15#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
17pub struct SurfaceEnergy {
18 pub hkl: [i32; 3],
20 pub energy: f64,
22}
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct WulffFacet {
27 pub hkl: [i32; 3],
29 pub normal: [f64; 3],
31 pub area: f64,
33 pub fraction: f64,
35 pub energy: f64,
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41pub struct WulffShape {
42 pub vertices: Vec<[f64; 3]>,
44 pub facets: Vec<WulffFacet>,
46 pub volume: f64,
48 pub surface_area: f64,
50}
51
52pub fn wulff(lattice: &Lattice, energies: &[SurfaceEnergy]) -> Result<WulffShape, Error> {
62 if energies.is_empty() {
63 return Err(Error::InvalidInput(
64 "the Wulff construction needs at least one surface energy".to_owned(),
65 ));
66 }
67 let reciprocal = lattice.reciprocal(false)?;
68 let matrix = reciprocal.matrix();
69 let mut planes = Vec::with_capacity(energies.len());
70 let mut normals = Vec::with_capacity(energies.len());
71 for surface in energies {
72 if !surface.energy.is_finite() || surface.energy <= 0.0 {
73 return Err(Error::InvalidInput(format!(
74 "surface energy for {:?} must be positive and finite",
75 surface.hkl
76 )));
77 }
78 let [h, k, l] = surface.hkl.map(f64::from);
79 let normal = [
80 h * matrix[0][0] + k * matrix[1][0] + l * matrix[2][0],
81 h * matrix[0][1] + k * matrix[1][1] + l * matrix[2][1],
82 h * matrix[0][2] + k * matrix[1][2] + l * matrix[2][2],
83 ];
84 let length = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
85 if length < 1e-12 {
86 return Err(Error::InvalidInput(format!(
87 "direction {:?} has no reciprocal vector",
88 surface.hkl
89 )));
90 }
91 planes.push(HalfSpace::new(normal, surface.energy)?);
92 normals.push((
93 [normal[0] / length, normal[1] / length, normal[2] / length],
94 surface.hkl,
95 surface.energy,
96 ));
97 }
98 let polyhedron = Polyhedron::from_half_spaces(&planes)?;
99 let total = polyhedron.surface_area();
100 if total <= 0.0 {
101 return Err(Error::InvalidInput(
102 "the Wulff shape has no surface".to_owned(),
103 ));
104 }
105 let mut facets = Vec::new();
106 for (face, _) in polyhedron.faces.iter().enumerate() {
107 let Some(normal) = polyhedron.face_normal(face) else {
108 continue;
109 };
110 let mut best: Option<(f64, [i32; 3], f64)> = None;
111 for (candidate, hkl, energy) in &normals {
112 let cosine =
113 normal[0] * candidate[0] + normal[1] * candidate[1] + normal[2] * candidate[2];
114 if cosine > best.map_or(0.99, |(value, _, _)| value) {
115 best = Some((cosine, *hkl, *energy));
116 }
117 }
118 let Some((_, hkl, energy)) = best else {
119 continue;
120 };
121 let area = polyhedron.face_area(face);
122 facets.push(WulffFacet {
123 hkl,
124 normal,
125 area,
126 fraction: area / total,
127 energy,
128 });
129 }
130 facets.sort_by(|first, second| {
131 second
132 .area
133 .partial_cmp(&first.area)
134 .unwrap_or(std::cmp::Ordering::Equal)
135 .then_with(|| first.hkl.cmp(&second.hkl))
136 });
137 let volume = polyhedron.volume();
138 Ok(WulffShape {
139 vertices: polyhedron.vertices,
140 facets,
141 volume,
142 surface_area: total,
143 })
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(rename_all = "snake_case")]
149pub enum AdsorptionKind {
150 Top,
152 Bridge,
154 Hollow,
156}
157
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub struct AdsorptionSite {
161 pub kind: AdsorptionKind,
163 pub position: [f64; 3],
165 pub frac: [f64; 3],
167 pub atoms: Vec<usize>,
169}
170
171pub fn adsorption_sites(slab: &Structure, tolerance: f64) -> Result<Vec<AdsorptionSite>, Error> {
178 if !tolerance.is_finite() || tolerance <= 0.0 {
179 return Err(Error::InvalidInput(
180 "adsorption tolerance must be positive and finite".to_owned(),
181 ));
182 }
183 let p1 = slab.p1()?;
184 if p1.atoms.is_empty() {
185 return Err(Error::InvalidInput("the slab has no atoms".to_owned()));
186 }
187 let top = p1
188 .atoms
189 .iter()
190 .map(|atom| atom.frac[2])
191 .fold(f64::NEG_INFINITY, f64::max);
192 let surface: Vec<usize> = p1
193 .atoms
194 .iter()
195 .enumerate()
196 .filter(|(_, atom)| (atom.frac[2] - top).abs() <= tolerance)
197 .map(|(index, _)| index)
198 .collect();
199 let mut sites: Vec<AdsorptionSite> = Vec::new();
200 for &index in &surface {
201 let Some(atom) = p1.atoms.get(index) else {
202 continue;
203 };
204 let frac = [atom.frac[0], atom.frac[1], top];
205 sites.push(AdsorptionSite {
206 kind: AdsorptionKind::Top,
207 position: p1.lattice.to_cart(frac),
208 frac,
209 atoms: vec![index],
210 });
211 }
212 let mut bonds = Vec::new();
213 for (position, &first) in surface.iter().enumerate() {
214 for &second in surface.iter().skip(position + 1) {
215 let (Some(a), Some(b)) = (p1.atoms.get(first), p1.atoms.get(second)) else {
216 continue;
217 };
218 let distance = p1.lattice.distance(a.frac, b.frac);
219 let radius = crate::elements::element_by_symbol(&a.element)?.covalent_radius
220 + crate::elements::element_by_symbol(&b.element)?.covalent_radius;
221 if distance <= 1.25 * radius {
222 bonds.push((first, second, distance));
223 let frac = [
224 (a.frac[0] + b.frac[0]) / 2.0,
225 (a.frac[1] + b.frac[1]) / 2.0,
226 top,
227 ];
228 sites.push(AdsorptionSite {
229 kind: AdsorptionKind::Bridge,
230 position: p1.lattice.to_cart(frac),
231 frac,
232 atoms: vec![first, second],
233 });
234 }
235 }
236 }
237 for (position, &first) in surface.iter().enumerate() {
238 for (other, &second) in surface.iter().enumerate().skip(position + 1) {
239 for &third in surface.iter().skip(other + 1) {
240 let has = |a: usize, b: usize| {
241 bonds.iter().any(|entry| {
242 (entry.0 == a && entry.1 == b) || (entry.0 == b && entry.1 == a)
243 })
244 };
245 if !(has(first, second) && has(second, third) && has(first, third)) {
246 continue;
247 }
248 let (Some(a), Some(b), Some(c)) = (
249 p1.atoms.get(first),
250 p1.atoms.get(second),
251 p1.atoms.get(third),
252 ) else {
253 continue;
254 };
255 let frac = [
256 (a.frac[0] + b.frac[0] + c.frac[0]) / 3.0,
257 (a.frac[1] + b.frac[1] + c.frac[1]) / 3.0,
258 top,
259 ];
260 sites.push(AdsorptionSite {
261 kind: AdsorptionKind::Hollow,
262 position: p1.lattice.to_cart(frac),
263 frac,
264 atoms: vec![first, second, third],
265 });
266 }
267 }
268 }
269 deduplicate(&mut sites, tolerance);
270 Ok(sites)
271}
272
273pub fn place_molecule(
284 slab: &Structure,
285 molecule: &Structure,
286 site: &AdsorptionSite,
287 offset: f64,
288) -> Result<Structure, Error> {
289 if !offset.is_finite() {
290 return Err(Error::InvalidInput(
291 "the adsorption offset must be finite".to_owned(),
292 ));
293 }
294 let molecule_p1 = molecule.p1()?;
295 if molecule_p1.atoms.is_empty() {
296 return Err(Error::InvalidInput("the molecule has no atoms".to_owned()));
297 }
298 let normal = slab.lattice().plane_normal([0.0, 0.0, 1.0])?;
299 let target = [
300 site.position[0] + normal[0] * offset,
301 site.position[1] + normal[1] * offset,
302 site.position[2] + normal[2] * offset,
303 ];
304 let mut centroid = [0.0; 3];
305 for atom in &molecule_p1.atoms {
306 let cart = molecule_p1.lattice.to_cart(atom.frac);
307 centroid[0] += cart[0];
308 centroid[1] += cart[1];
309 centroid[2] += cart[2];
310 }
311 let count = molecule_p1.atoms.len() as f64;
312 centroid = [
313 centroid[0] / count,
314 centroid[1] / count,
315 centroid[2] / count,
316 ];
317 let mut builder = StructureBuilder::new(slab.name(), *slab.lattice()).hall(slab.hall());
318 for operation in slab.operations() {
319 builder = builder.operation(*operation);
320 }
321 for site in slab.sites() {
322 builder = builder.site(site.clone());
323 }
324 for atom in &molecule_p1.atoms {
325 let cart = molecule_p1.lattice.to_cart(atom.frac);
326 let moved = [
327 cart[0] - centroid[0] + target[0],
328 cart[1] - centroid[1] + target[1],
329 cart[2] - centroid[2] + target[2],
330 ];
331 let frac = slab.lattice().to_frac(moved);
332 builder = builder.site(
333 Site::new(atom.label.clone(), &atom.element, frac)?.with_occupancy(atom.occupancy)?,
334 );
335 }
336 for (key, value) in slab.metadata() {
337 builder = builder.metadata(key.clone(), value.clone());
338 }
339 builder.build()
340}
341
342fn deduplicate(sites: &mut Vec<AdsorptionSite>, tolerance: f64) {
343 let mut kept: Vec<AdsorptionSite> = Vec::new();
344 for site in sites.drain(..) {
345 let duplicate = kept.iter().any(|existing| {
346 existing.kind == site.kind
347 && existing.atoms.iter().all(|atom| site.atoms.contains(atom))
348 && distance(existing.position, site.position) <= tolerance
349 });
350 if !duplicate {
351 kept.push(site);
352 }
353 }
354 kept.sort_by(|first, second| {
355 kind_order(first.kind)
356 .cmp(&kind_order(second.kind))
357 .then_with(|| {
358 first
359 .position
360 .partial_cmp(&second.position)
361 .unwrap_or(std::cmp::Ordering::Equal)
362 })
363 });
364 *sites = kept;
365}
366
367fn kind_order(kind: AdsorptionKind) -> u8 {
368 match kind {
369 AdsorptionKind::Top => 0,
370 AdsorptionKind::Bridge => 1,
371 AdsorptionKind::Hollow => 2,
372 }
373}
374
375fn distance(first: [f64; 3], second: [f64; 3]) -> f64 {
376 let dx = first[0] - second[0];
377 let dy = first[1] - second[1];
378 let dz = first[2] - second[2];
379 (dx * dx + dy * dy + dz * dz).sqrt()
380}