oxiphysics_core/multiscale_methods/
functions.rs1#![allow(clippy::needless_range_loop, clippy::ptr_arg)]
6use std::f64::consts::PI;
7
8use super::types::{Atom, HmmConfig, HomogenizationResult, NebImage, PhaseFieldParams, UnitCell};
9
10#[inline]
12pub fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
13 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
14}
15#[inline]
17pub fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
18 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
19}
20#[inline]
22pub fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
23 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
24}
25#[inline]
27pub fn scale3(v: [f64; 3], s: f64) -> [f64; 3] {
28 [v[0] * s, v[1] * s, v[2] * s]
29}
30#[inline]
32pub fn norm3(v: [f64; 3]) -> f64 {
33 dot3(v, v).sqrt()
34}
35#[inline]
37pub fn normalize3(v: [f64; 3]) -> [f64; 3] {
38 let n = norm3(v);
39 if n < 1e-300 {
40 [0.0; 3]
41 } else {
42 scale3(v, 1.0 / n)
43 }
44}
45#[inline]
47pub fn mat3_vec(m: &[f64; 9], v: [f64; 3]) -> [f64; 3] {
48 [
49 m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
50 m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
51 m[6] * v[0] + m[7] * v[1] + m[8] * v[2],
52 ]
53}
54#[inline]
56pub fn mat3_add(a: &[f64; 9], b: &[f64; 9]) -> [f64; 9] {
57 let mut r = [0.0f64; 9];
58 for i in 0..9 {
59 r[i] = a[i] + b[i];
60 }
61 r
62}
63#[inline]
65pub fn mat3_scale(m: &[f64; 9], s: f64) -> [f64; 9] {
66 let mut r = [0.0f64; 9];
67 for i in 0..9 {
68 r[i] = m[i] * s;
69 }
70 r
71}
72#[inline]
74pub fn mat3_identity() -> [f64; 9] {
75 [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]
76}
77#[inline]
79pub fn mat3_frob(m: &[f64; 9]) -> f64 {
80 m.iter().map(|x| x * x).sum::<f64>().sqrt()
81}
82pub fn homogenize_two_phase(cell: &UnitCell, prop1: f64, prop2: f64) -> HomogenizationResult {
92 let n = cell.property.len() as f64;
93 let n1 = cell
94 .property
95 .iter()
96 .filter(|&&p| (p - prop1).abs() < 1e-10)
97 .count();
98 let f1 = n1 as f64 / n;
99 let f2 = 1.0 - f1;
100 let voigt = f1 * prop1 + f2 * prop2;
101 let reuss = 1.0 / (f1 / prop1 + f2 / prop2);
102 let hs_upper = prop1 + f2 / (1.0 / (prop2 - prop1) + f1 / (3.0 * prop1));
103 let hs_lower = prop2 + f1 / (1.0 / (prop1 - prop2) + f2 / (3.0 * prop2));
104 let effective = 0.5 * (hs_upper + hs_lower);
105 HomogenizationResult {
106 effective_property: effective,
107 voigt_bound: voigt,
108 reuss_bound: reuss,
109 hs_upper,
110 hs_lower,
111 volume_fraction: f1,
112 }
113}
114pub fn cell_problem_1d(cell: &UnitCell, max_iter: usize, tol: f64) -> Vec<f64> {
125 let n = cell.nx;
126 let mut chi = vec![0.0f64; n];
127 for _iter in 0..max_iter {
128 let mut max_change = 0.0f64;
129 for i in 0..n {
130 let im = (i + n - 1) % n;
131 let ip = (i + 1) % n;
132 let km = 0.5 * (cell.property[i] + cell.property[im]);
133 let kp = 0.5 * (cell.property[i] + cell.property[ip]);
134 let rhs = (kp - km) / cell.h;
135 let new_val = (km * chi[im] + kp * chi[ip] - rhs * cell.h * cell.h) / (km + kp);
136 max_change = max_change.max((new_val - chi[i]).abs());
137 chi[i] = new_val;
138 }
139 if max_change < tol {
140 break;
141 }
142 }
143 chi
144}
145pub fn hmm_estimate_flux<F>(config: &HmmConfig, macro_strain: f64, property_fn: F) -> f64
156where
157 F: Fn(f64) -> f64,
158{
159 let n = 16usize;
160 let h = config.micro_domain_size / n as f64;
161 let mut t = vec![0.0f64; n];
162 for i in 0..n {
163 let x = (i as f64 + 0.5) * h;
164 t[i] = macro_strain * x;
165 }
166 for _step in 0..config.micro_steps {
167 let mut t_new = t.clone();
168 for i in 1..(n - 1) {
169 let x = (i as f64) * h;
170 let km = property_fn(x - 0.5 * h);
171 let kp = property_fn(x + 0.5 * h);
172 t_new[i] = (km * t[i - 1] + kp * t[i + 1]) / (km + kp);
173 }
174 t = t_new;
175 }
176 let flux_sum: f64 = (0..n - 1)
177 .map(|i| {
178 let x = (i as f64 + 0.5) * h;
179 let k = property_fn(x);
180 -k * (t[i + 1] - t[i]) / h
181 })
182 .sum();
183 flux_sum / (n - 1) as f64
184}
185pub fn lennard_jones_potential(r: f64, epsilon: f64, sigma: f64) -> f64 {
189 let s_over_r = sigma / r.max(1e-15);
190 let sr6 = s_over_r.powi(6);
191 4.0 * epsilon * (sr6 * sr6 - sr6)
192}
193pub fn lennard_jones_force(r: f64, epsilon: f64, sigma: f64) -> f64 {
197 let s_over_r = sigma / r.max(1e-15);
198 let sr6 = s_over_r.powi(6);
199 24.0 * epsilon / r * (2.0 * sr6 * sr6 - sr6)
200}
201pub fn cauchy_born_stress_1d(
209 f_deformation: f64,
210 epsilon: f64,
211 sigma: f64,
212 lattice_const: f64,
213) -> f64 {
214 let r0 = lattice_const;
215 let r = f_deformation * r0;
216 let force = lennard_jones_force(r, epsilon, sigma);
217 force * f_deformation
218}
219pub fn virial_stress_tensor(
231 atoms: &[Atom],
232 pairs: &[(usize, usize)],
233 epsilon: f64,
234 sigma: f64,
235 volume: f64,
236) -> [f64; 9] {
237 let mut sigma_v = [0.0f64; 9];
238 for &(i, j) in pairs {
239 if i >= atoms.len() || j >= atoms.len() {
240 continue;
241 }
242 let r_vec = sub3(atoms[j].pos, atoms[i].pos);
243 let r = norm3(r_vec);
244 if r < 1e-12 {
245 continue;
246 }
247 let f_mag = lennard_jones_force(r, epsilon, sigma);
248 let f_vec = scale3(normalize3(r_vec), f_mag);
249 for a in 0..3 {
250 for b in 0..3 {
251 sigma_v[a * 3 + b] -= r_vec[a] * f_vec[b];
252 }
253 }
254 }
255 mat3_scale(&sigma_v, 1.0 / volume.max(1e-300))
256}
257pub fn solve_macro_diffusion_1d(
272 n: usize,
273 length: f64,
274 k_eff: &[f64],
275 source: &[f64],
276 t_left: f64,
277 t_right: f64,
278) -> Vec<f64> {
279 let h = length / (n + 1) as f64;
280 let mut t = vec![0.0f64; n + 2];
281 t[0] = t_left;
282 t[n + 1] = t_right;
283 for _iter in 0..10_000 {
284 let mut max_change = 0.0f64;
285 for i in 1..=n {
286 let ii = i - 1;
287 let km = if ii == 0 {
288 k_eff[0]
289 } else {
290 0.5 * (k_eff[ii] + k_eff[ii - 1])
291 };
292 let kp = if ii + 1 >= k_eff.len() {
293 k_eff[k_eff.len() - 1]
294 } else {
295 0.5 * (k_eff[ii] + k_eff[ii + 1])
296 };
297 let rhs = source[ii] * h * h;
298 let t_new = (km * t[i - 1] + kp * t[i + 1] + rhs) / (km + kp);
299 max_change = max_change.max((t_new - t[i]).abs());
300 t[i] = t_new;
301 }
302 if max_change < 1e-12 {
303 break;
304 }
305 }
306 t
307}
308pub fn centre_of_mass_velocity(atoms: &[Atom]) -> [f64; 3] {
310 let mut mom = [0.0f64; 3];
311 let mut total_mass = 0.0f64;
312 for a in atoms {
313 mom = add3(mom, scale3(a.vel, a.mass));
314 total_mass += a.mass;
315 }
316 if total_mass < 1e-300 {
317 [0.0; 3]
318 } else {
319 scale3(mom, 1.0 / total_mass)
320 }
321}
322pub fn atomistic_temperature(atoms: &[Atom], k_boltzmann: f64) -> f64 {
325 let ke: f64 = atoms.iter().map(|a| a.kinetic_energy()).sum();
326 let n = atoms.len() as f64;
327 if n < 1.0 || k_boltzmann < 1e-300 {
328 0.0
329 } else {
330 2.0 * ke / (3.0 * n * k_boltzmann)
331 }
332}
333pub fn mean_squared_displacement(atoms: &[Atom], reference: &[[f64; 3]]) -> f64 {
336 let n = atoms.len().min(reference.len());
337 if n == 0 {
338 return 0.0;
339 }
340 let msd: f64 = atoms
341 .iter()
342 .take(n)
343 .zip(reference.iter())
344 .map(|(a, r)| {
345 let d = sub3(a.pos, *r);
346 dot3(d, d)
347 })
348 .sum();
349 msd / n as f64
350}
351pub fn restriction_l2(fine: &[f64], ratio: usize) -> Vec<f64> {
354 fine.chunks(ratio)
355 .map(|chunk| chunk.iter().sum::<f64>() / chunk.len() as f64)
356 .collect()
357}
358pub fn prolongation_linear(coarse: &[f64], ratio: usize) -> Vec<f64> {
361 let n_fine = coarse.len() * ratio;
362 let mut fine = vec![0.0f64; n_fine];
363 for fi in 0..n_fine {
364 let ci_f = fi as f64 / ratio as f64;
365 let ci0 = (ci_f as usize).min(coarse.len() - 1);
366 let ci1 = (ci0 + 1).min(coarse.len() - 1);
367 let alpha = ci_f - ci0 as f64;
368 fine[fi] = (1.0 - alpha) * coarse[ci0] + alpha * coarse[ci1];
369 }
370 fine
371}
372pub fn multigrid_vcycle(u: &mut Vec<f64>, f: &[f64], h: f64, n_smooth: usize, depth: usize) {
384 let n = u.len();
385 for _ in 0..n_smooth {
386 for i in 1..(n - 1) {
387 u[i] = 0.5 * (u[i - 1] + u[i + 1] - h * h * f[i]);
388 }
389 }
390 if depth == 0 || n <= 3 {
391 return;
392 }
393 let mut res = vec![0.0f64; n];
394 for i in 1..(n - 1) {
395 res[i] = f[i] - (u[i + 1] - 2.0 * u[i] + u[i - 1]) / (h * h);
396 }
397 let coarse_res = restriction_l2(&res, 2);
398 let nc = coarse_res.len();
399 let mut e_coarse = vec![0.0f64; nc];
400 multigrid_vcycle(&mut e_coarse, &coarse_res, 2.0 * h, n_smooth, depth - 1);
401 let e_fine = prolongation_linear(&e_coarse, 2);
402 for i in 0..n.min(e_fine.len()) {
403 u[i] += e_fine[i];
404 }
405 for _ in 0..n_smooth {
406 for i in 1..(n - 1) {
407 u[i] = 0.5 * (u[i - 1] + u[i + 1] - h * h * f[i]);
408 }
409 }
410}
411pub fn allen_cahn_step(phi: &mut Vec<f64>, params: &PhaseFieldParams, dx: f64, dt: f64) {
423 let n = phi.len();
424 let mut dphi = vec![0.0f64; n];
425 for i in 0..n {
426 let im = (i + n - 1) % n;
427 let ip = (i + 1) % n;
428 let laplacian = (phi[im] - 2.0 * phi[i] + phi[ip]) / (dx * dx);
429 let p = phi[i];
430 let dw = 2.0 * params.well_height * p * (p - 1.0) * (2.0 * p - 1.0);
431 dphi[i] = params.mobility * (params.epsilon_sq * laplacian - dw);
432 }
433 for i in 0..n {
434 phi[i] += dt * dphi[i];
435 phi[i] = phi[i].clamp(0.0, 1.0);
436 }
437}
438pub fn umbrella_free_energy(samples: &[f64], xi_0: f64, k_bias: f64, beta: f64) -> f64 {
449 let n = samples.len() as f64;
450 if n < 1.0 {
451 return 0.0;
452 }
453 let mean_bias: f64 = samples
454 .iter()
455 .map(|&xi| 0.5 * k_bias * (xi - xi_0).powi(2))
456 .sum::<f64>()
457 / n;
458 let mean_xi: f64 = samples.iter().sum::<f64>() / n;
459 let var_xi: f64 = samples
460 .iter()
461 .map(|&xi| (xi - mean_xi).powi(2))
462 .sum::<f64>()
463 / n;
464 let entropy_term = if var_xi > 1e-300 {
465 -0.5 * (2.0 * PI * var_xi).ln() / beta
466 } else {
467 0.0
468 };
469 entropy_term + mean_bias
470}
471pub fn check_scale_separation(micro_length: f64, macro_length: f64, factor: f64) -> bool {
474 macro_length >= factor * micro_length
475}
476pub fn hill_mandel_error(
481 micro_stress: &[f64],
482 micro_strain: &[f64],
483 macro_stress: [f64; 9],
484 macro_strain: [f64; 9],
485) -> f64 {
486 let n = micro_stress.len().min(micro_strain.len()) as f64;
487 if n < 1.0 {
488 return 0.0;
489 }
490 let micro_power: f64 = micro_stress
491 .iter()
492 .zip(micro_strain.iter())
493 .map(|(s, e)| s * e)
494 .sum::<f64>()
495 / n;
496 let macro_power: f64 = macro_stress
497 .iter()
498 .zip(macro_strain.iter())
499 .map(|(s, e)| s * e)
500 .sum();
501 (micro_power - macro_power).abs() / macro_power.abs().max(1e-300)
502}
503pub fn cg_bond_force(pos_a: [f64; 3], pos_b: [f64; 3], k: f64, r0: f64) -> [f64; 3] {
509 let r_vec = sub3(pos_b, pos_a);
510 let r = norm3(r_vec);
511 if r < 1e-12 {
512 return [0.0; 3];
513 }
514 let f_mag = k * (r - r0);
515 scale3(normalize3(r_vec), f_mag)
516}
517pub fn cauchy_born_modulus(bond_stiffnesses: &[f64], bond_lengths: &[f64], volume: f64) -> f64 {
524 let n = bond_stiffnesses.len().min(bond_lengths.len());
525 let sum: f64 = (0..n)
526 .map(|i| bond_stiffnesses[i] * bond_lengths[i].powi(2))
527 .sum();
528 sum / volume.max(1e-300)
529}
530pub fn arlequin_blend_energy(e_atomistic: f64, e_continuum: f64, weight: f64) -> f64 {
538 let w = weight.clamp(0.0, 1.0);
539 w * e_atomistic + (1.0 - w) * e_continuum
540}
541#[allow(clippy::too_many_arguments)]
549pub fn neb_step<F>(images: &mut Vec<NebImage>, spring_constant: f64, grad_fn: &F, dt: f64)
550where
551 F: Fn(&[f64]) -> (f64, Vec<f64>),
552{
553 let n = images.len();
554 if n < 3 {
555 return;
556 }
557 for img in images.iter_mut() {
558 let (e, g) = grad_fn(&img.coords);
559 img.energy = e;
560 img.force = g;
561 }
562 for i in 1..(n - 1) {
563 let prev = images[i - 1].coords.clone();
564 let next = images[i + 1].coords.clone();
565 let curr = images[i].coords.clone();
566 let tau: Vec<f64> = curr
567 .iter()
568 .zip(prev.iter())
569 .zip(next.iter())
570 .map(|((&c, &p), &nx)| {
571 let dp = c - p;
572 let dn = nx - c;
573 0.5 * (dn + dp)
574 })
575 .collect();
576 let tau_norm: f64 = tau.iter().map(|t| t * t).sum::<f64>().sqrt().max(1e-300);
577 let d_prev: f64 = curr
578 .iter()
579 .zip(prev.iter())
580 .map(|(&c, &p)| (c - p).powi(2))
581 .sum::<f64>()
582 .sqrt();
583 let d_next: f64 = next
584 .iter()
585 .zip(curr.iter())
586 .map(|(&nx, &c)| (nx - c).powi(2))
587 .sum::<f64>()
588 .sqrt();
589 let f_spring = spring_constant * (d_next - d_prev);
590 let f_real = images[i].force.clone();
591 let f_dot_tau: f64 = f_real
592 .iter()
593 .zip(tau.iter())
594 .map(|(&f, &t)| f * t / tau_norm)
595 .sum();
596 let f_perp: Vec<f64> = f_real
597 .iter()
598 .zip(tau.iter())
599 .map(|(&f, &t)| f - f_dot_tau * t / tau_norm)
600 .collect();
601 let f_spring_vec: Vec<f64> = tau.iter().map(|&t| f_spring * t / tau_norm).collect();
602 let f_neb: Vec<f64> = f_perp
603 .iter()
604 .zip(f_spring_vec.iter())
605 .map(|(fp, fs)| fp + fs)
606 .collect();
607 for (c, f) in images[i].coords.iter_mut().zip(f_neb.iter()) {
608 *c += dt * f;
609 }
610 }
611}