1use std::fmt;
8
9use crate::Error;
10use crate::lattice::Lattice;
11
12pub const UNDO_ATOMS: u64 = 2_000_000;
14pub const UNDO_KEEP: u64 = 5;
16pub const HESSIAN_MIB_PER_ATOM: f64 = 13.0;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum Profile {
22 #[default]
24 Standard,
25 Generous,
27 WarnOnly,
29}
30
31impl Profile {
32 #[must_use]
34 pub fn parse(name: &str) -> Self {
35 match name {
36 "generous" => Self::Generous,
37 "warn_only" | "warn-only" => Self::WarnOnly,
38 _ => Self::Standard,
39 }
40 }
41
42 #[must_use]
44 pub const fn name(self) -> &'static str {
45 match self {
46 Self::Standard => "standard",
47 Self::Generous => "generous",
48 Self::WarnOnly => "warn_only",
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum Budget {
56 Drawn,
58 Supercell,
60 Grid,
62 Carbon,
64 Nanotube,
66 Graphene,
68 Phonons,
70 Hessian,
72}
73
74impl Budget {
75 pub fn parse(name: &str) -> Result<Self, Error> {
81 match name {
82 "drawn" => Ok(Self::Drawn),
83 "supercell" => Ok(Self::Supercell),
84 "grid" => Ok(Self::Grid),
85 "carbon" => Ok(Self::Carbon),
86 "nanotube" => Ok(Self::Nanotube),
87 "graphene" => Ok(Self::Graphene),
88 "phonons" => Ok(Self::Phonons),
89 "hessian" => Ok(Self::Hessian),
90 _ => Err(Error::InvalidInput(format!("unknown limit budget: {name}"))),
91 }
92 }
93
94 fn table(self) -> (u64, u64) {
95 match self {
96 Self::Drawn => (100_000, 400_000),
97 Self::Supercell => (50_000, 200_000),
98 Self::Grid => (1_500_000, 4_000_000),
99 Self::Carbon => (10_000, 30_000),
100 Self::Nanotube => (50_000, 200_000),
101 Self::Graphene => (50_000, 200_000),
102 Self::Phonons => (2_000, 8_000),
103 Self::Hessian => (100, 300),
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum LimitLevel {
111 Ok,
113 Warn,
115 Refuse,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct Verdict {
122 pub level: LimitLevel,
124 pub estimate: u64,
126 pub sentence: String,
128}
129
130impl LimitLevel {
131 #[must_use]
133 pub const fn name(self) -> &'static str {
134 match self {
135 Self::Ok => "ok",
136 Self::Warn => "warn",
137 Self::Refuse => "refuse",
138 }
139 }
140}
141
142impl Verdict {
143 #[must_use]
145 pub fn refused(&self) -> bool {
146 self.level == LimitLevel::Refuse
147 }
148
149 #[must_use]
151 pub fn warned(&self) -> bool {
152 self.level != LimitLevel::Ok
153 }
154}
155
156impl fmt::Display for Verdict {
157 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
158 if self.sentence.is_empty() {
159 write!(formatter, "{}", self.estimate)
160 } else {
161 formatter.write_str(&self.sentence)
162 }
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
168pub struct Limits {
169 profile: Profile,
170}
171
172impl Limits {
173 #[must_use]
175 pub const fn new(profile: Profile) -> Self {
176 Self { profile }
177 }
178
179 #[must_use]
181 pub const fn profile(&self) -> Profile {
182 self.profile
183 }
184
185 #[must_use]
187 pub fn bounds(&self, budget: Budget) -> (u64, Option<u64>) {
188 let (soft, hard) = budget.table();
189 match self.profile {
190 Profile::Standard => (soft, Some(hard)),
191 Profile::Generous => (2 * soft, Some(2 * hard)),
192 Profile::WarnOnly => (soft, None),
193 }
194 }
195
196 #[must_use]
198 pub fn judge(
199 &self,
200 budget: Budget,
201 estimate: u64,
202 what: &str,
203 fits: impl Fn(u64) -> String,
204 ) -> Verdict {
205 let (soft, hard) = self.bounds(budget);
206 if estimate <= soft {
207 return Verdict {
208 level: LimitLevel::Ok,
209 estimate,
210 sentence: String::new(),
211 };
212 }
213 if let Some(hard) = hard
214 && estimate > hard
215 {
216 return Verdict {
217 level: LimitLevel::Refuse,
218 estimate,
219 sentence: format!(
220 "{} over the {} the Large structures setting allows; {}",
221 what.replace("{}", &count(estimate)),
222 count(hard),
223 fits(hard)
224 ),
225 };
226 }
227 Verdict {
228 level: LimitLevel::Warn,
229 estimate,
230 sentence: format!(
231 "{} over the {} that is comfortable; {}",
232 what.replace("{}", &count(estimate)),
233 count(soft),
234 fits(soft)
235 ),
236 }
237 }
238}
239
240#[must_use]
242pub fn count(value: u64) -> String {
243 if value >= 1_000_000 {
244 format!("{:.1} M", value as f64 / 1e6)
245 } else {
246 let text = value.to_string();
247 let mut out = String::new();
248 for (index, character) in text.chars().rev().enumerate() {
249 if index > 0 && index % 3 == 0 {
250 out.push(',');
251 }
252 out.push(character);
253 }
254 out.chars().rev().collect()
255 }
256}
257
258pub fn limits_bounds(profile: &str, budget: &str) -> Result<(u64, Option<u64>), Error> {
264 let limits = Limits::new(Profile::parse(profile));
265 Ok(limits.bounds(Budget::parse(budget)?))
266}
267
268pub fn limits_check(profile: &str, budget: &str, estimate: u64) -> Result<Verdict, Error> {
274 let limits = Limits::new(Profile::parse(profile));
275 let budget = Budget::parse(budget)?;
276 Ok(limits.judge(
277 budget,
278 estimate,
279 "the operation would have {} items",
280 |limit| format!("up to {} items fit", count(limit)),
281 ))
282}
283
284#[must_use]
286pub fn drawn_atoms(n_atoms: u64, ranges: [(f64, f64); 3]) -> u64 {
287 let volume = ranges
288 .iter()
289 .map(|(low, high)| (high - low).max(0.0))
290 .product::<f64>();
291 (n_atoms as f64 * volume).round() as u64
292}
293
294pub fn multiplier(p: [[f64; 3]; 3]) -> Result<u64, Error> {
300 if !p.iter().flatten().all(|value| value.is_finite()) {
301 return Err(Error::InvalidInput(
302 "transformation matrix contains non-finite values".to_owned(),
303 ));
304 }
305 let [[a, b, c], [d, e, f], [g, h, i]] = p;
306 let determinant = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
307 if determinant.abs() < 1e-9 {
308 return Err(Error::InvalidInput(
309 "transformation matrix is singular".to_owned(),
310 ));
311 }
312 Ok(determinant.abs().round() as u64)
313}
314
315pub fn supercell_atoms(n_atoms: u64, p: [[f64; 3]; 3]) -> Result<u64, Error> {
321 Ok(n_atoms.saturating_mul(multiplier(p)?))
322}
323
324pub fn grid_shape(lattice: &Lattice, spacing: f64) -> Result<[u32; 3], Error> {
330 if !spacing.is_finite() || spacing <= 0.0 {
331 return Err(Error::InvalidInput(format!(
332 "grid spacing must be positive and finite, got {spacing}"
333 )));
334 }
335 let spacing = spacing.max(0.05);
336 Ok(lattice
337 .lengths()
338 .map(|length| (length / spacing).round().max(8.0) as u32))
339}
340
341pub fn grid_points(lattice: &Lattice, spacing: f64) -> Result<u64, Error> {
347 let [a, b, c] = grid_shape(lattice, spacing)?;
348 Ok(u64::from(a) * u64::from(b) * u64::from(c))
349}
350
351fn cube_that_fits(per_cell: f64, limit: u64, cells: &str) -> String {
352 let k = ((limit as f64 / per_cell.max(1.0)).powf(1.0 / 3.0) + 1e-9).floor() as u64;
353 if k < 1 {
354 format!("one cell alone is {} atoms", count(per_cell.round() as u64))
355 } else {
356 format!("{k} x {k} x {k}{cells} is the largest that fits")
357 }
358}
359
360#[must_use]
362pub fn check_drawn(limits: &Limits, n_atoms: u64, ranges: [(f64, f64); 3]) -> Verdict {
363 let estimate = drawn_atoms(n_atoms, ranges);
364 limits.judge(
365 Budget::Drawn,
366 estimate,
367 "the picture would draw {} atoms",
368 |limit| cube_that_fits(n_atoms as f64, limit, " cells"),
369 )
370}
371
372pub fn check_supercell(limits: &Limits, n_atoms: u64, p: [[f64; 3]; 3]) -> Result<Verdict, Error> {
378 let estimate = supercell_atoms(n_atoms, p)?;
379 Ok(limits.judge(
380 Budget::Supercell,
381 estimate,
382 "the new cell would have {} atoms",
383 |limit| cube_that_fits(n_atoms as f64, limit, ""),
384 ))
385}
386
387pub fn check_grid(limits: &Limits, lattice: &Lattice, spacing: f64) -> Result<Verdict, Error> {
393 let estimate = grid_points(lattice, spacing)?;
394 Ok(limits.judge(
395 Budget::Grid,
396 estimate,
397 "the grid would have {} points",
398 |limit| {
399 let mut step = (spacing / 0.05).ceil() * 0.05;
400 step = step.max(0.05);
401 while grid_points(lattice, step).unwrap_or(u64::MAX) > limit && step < 5.0 {
402 step += 0.05;
403 }
404 format!(
405 "at {step:.2} A it would have {}",
406 grid_points(lattice, step)
407 .map(count)
408 .unwrap_or_else(|_| "an invalid count".to_owned())
409 )
410 },
411 ))
412}
413
414pub fn check_phonons(limits: &Limits, n_atoms: u64, p: [[f64; 3]; 3]) -> Result<Verdict, Error> {
420 let estimate = supercell_atoms(n_atoms, p)?;
421 Ok(limits.judge(
422 Budget::Phonons,
423 estimate,
424 "the phonon supercell would have {} atoms",
425 |limit| {
426 format!(
427 "{}, or Gamma only needs none",
428 cube_that_fits(n_atoms as f64, limit, "")
429 )
430 },
431 ))
432}
433
434#[must_use]
436pub fn check_hessian(limits: &Limits, n_atoms: u64) -> Verdict {
437 limits.judge(
438 Budget::Hessian,
439 n_atoms,
440 &format!(
441 "an analytic Hessian over {{}} atoms would hold about {:.1} GB",
442 n_atoms as f64 * HESSIAN_MIB_PER_ATOM / 1024.0
443 ),
444 |limit| {
445 format!(
446 "up to {} atoms fit, and finite differences need no more than one evaluation does",
447 count(limit)
448 )
449 },
450 )
451}