Skip to main content

oxiphysics_materials/
materials_bench.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Benchmark suite for the OxiPhysics materials constitutive kernels.
5//!
6//! Measures batch throughput (MFLOP/s or Mpoints/s) for each kernel in
7//! [`crate::simd_paths`], covering:
8//!
9//! - Linear isotropic elasticity
10//! - Von Mises plasticity
11//! - Neo-Hookean hyperelasticity
12//! - Viscoplasticity (Perzyna)
13//! - Miner's rule fatigue damage
14//! - Thermo-elastic coupling
15//! - J2 return-mapping
16//!
17//! ## Usage
18//!
19//! ```
20//! use oxiphysics_materials::materials_bench::{MatBenchHarness, bench_elastic};
21//!
22//! let mut h = MatBenchHarness::new();
23//!
24//! let r = h.run("elastic_n1000", || bench_elastic(1000));
25//! println!("{}", r);
26//! ```
27
28#![allow(dead_code)]
29
30use std::time::{Duration, Instant};
31
32use crate::simd_paths::{
33    SoaMaterialPoints, elastic_stress_batch, elastic_stress_batch_x4, miner_damage_batch,
34    neo_hookean_stress_batch, return_mapping_batch, thermal_expansion_stress_batch,
35    viscoplastic_rate_batch, von_mises_yield_batch,
36};
37
38// ── MatBenchReport ────────────────────────────────────────────────────────────
39
40/// Result of one material batch kernel benchmark run.
41#[derive(Debug, Clone)]
42pub struct MatBenchReport {
43    /// Human-readable kernel name.
44    pub name: String,
45    /// Number of timed iterations.
46    pub iterations: u32,
47    /// Number of material integration points.
48    pub n: usize,
49    /// Total wall-clock time for all iterations.
50    pub total: Duration,
51    /// Mean time per iteration.
52    pub mean: Duration,
53    /// Estimated throughput in MFLOP/s (kernel-specific estimate).
54    pub mflops: Option<f64>,
55    /// Throughput in Mpoints/s (1e6 points/second).
56    pub mpoints_per_sec: f64,
57}
58
59impl std::fmt::Display for MatBenchReport {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        write!(
62            f,
63            "[{:<28}] n={:>7} mean={:.2}µs  {:.1} Mpts/s",
64            self.name,
65            self.n,
66            self.mean.as_secs_f64() * 1e6,
67            self.mpoints_per_sec
68        )?;
69        if let Some(mf) = self.mflops {
70            write!(f, "  ({:.1} MFLOPs)", mf)?;
71        }
72        Ok(())
73    }
74}
75
76// ── MatBenchHarness ───────────────────────────────────────────────────────────
77
78/// Timing harness for material constitutive kernel benchmarks.
79pub struct MatBenchHarness {
80    /// Number of warm-up iterations (not timed).
81    pub warmup: u32,
82    /// Number of timed iterations.
83    pub iterations: u32,
84    /// Collected reports.
85    pub reports: Vec<MatBenchReport>,
86}
87
88impl MatBenchHarness {
89    /// Create a harness with 2 warm-up and 5 timed iterations.
90    pub fn new() -> Self {
91        Self {
92            warmup: 2,
93            iterations: 5,
94            reports: Vec::new(),
95        }
96    }
97
98    /// Create a faster harness (1 warm-up, 3 timed).
99    pub fn fast() -> Self {
100        Self {
101            warmup: 1,
102            iterations: 3,
103            reports: Vec::new(),
104        }
105    }
106
107    /// Run closure `f` and record a `MatBenchReport`.
108    pub fn run<F>(&mut self, name: &str, mut f: F) -> MatBenchReport
109    where
110        F: FnMut() -> MatBenchResult,
111    {
112        let mut last = MatBenchResult::default();
113        for _ in 0..self.warmup {
114            last = f();
115        }
116
117        let t0 = Instant::now();
118        for _ in 0..self.iterations {
119            last = f();
120        }
121        let total = t0.elapsed();
122        let mean = total / self.iterations;
123
124        let sec = mean.as_secs_f64().max(1e-12);
125        let mpoints = last.n as f64 / sec / 1e6;
126
127        let r = MatBenchReport {
128            name: name.to_string(),
129            iterations: self.iterations,
130            n: last.n,
131            total,
132            mean,
133            mflops: last.mflops,
134            mpoints_per_sec: mpoints,
135        };
136        self.reports.push(r.clone());
137        r
138    }
139
140    /// Print a summary table of all collected reports.
141    pub fn print_summary(&self) {
142        println!("\n{:=<80}", "");
143        println!(
144            "{:<28} {:>8} {:>12} {:>12} {:>10}",
145            "Kernel", "N", "Mean (µs)", "Mpts/s", "MFLOPs"
146        );
147        println!("{:=<80}", "");
148        for r in &self.reports {
149            let mf = r.mflops.map_or("—".to_string(), |m| format!("{:.1}", m));
150            println!(
151                "{:<28} {:>8} {:>12.3} {:>12.1} {:>10}",
152                r.name,
153                r.n,
154                r.mean.as_secs_f64() * 1e6,
155                r.mpoints_per_sec,
156                mf
157            );
158        }
159        println!("{:=<80}", "");
160    }
161}
162
163impl Default for MatBenchHarness {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169// ── MatBenchResult ────────────────────────────────────────────────────────────
170
171/// Intermediate output from a benchmark closure.
172#[derive(Debug, Default, Clone)]
173pub struct MatBenchResult {
174    /// Problem size.
175    pub n: usize,
176    /// Optional MFLOP estimate.
177    pub mflops: Option<f64>,
178}
179
180// ── Kernel benchmarks ─────────────────────────────────────────────────────────
181
182/// Benchmark linear isotropic elasticity (scalar loop).
183///
184/// FLOPs ≈ 18 × n  (6 stress components, 3 additions each).
185pub fn bench_elastic(n: usize) -> MatBenchResult {
186    let mut pts = SoaMaterialPoints::new(n);
187    let e = 210e9_f64;
188    let nu = 0.3_f64;
189    // Uniform uniaxial strain
190    pts.strain_xx.fill(1e-3);
191    elastic_stress_batch(&mut pts, e, nu);
192    let flops = 18.0 * n as f64;
193    MatBenchResult {
194        n,
195        mflops: Some(flops / 1e6),
196    }
197}
198
199/// Benchmark linear isotropic elasticity (x4 unrolled loop).
200///
201/// Same physics as [`bench_elastic`], but using the unrolled 4-wide variant.
202pub fn bench_elastic_x4(n: usize) -> MatBenchResult {
203    let mut pts = SoaMaterialPoints::new(n);
204    let e = 210e9_f64;
205    let nu = 0.3_f64;
206    pts.strain_xx.fill(1e-3);
207    elastic_stress_batch_x4(&mut pts, e, nu);
208    let flops = 18.0 * n as f64;
209    MatBenchResult {
210        n,
211        mflops: Some(flops / 1e6),
212    }
213}
214
215/// Benchmark von Mises yield surface evaluation.
216///
217/// FLOPs ≈ 10 × n  (deviatoric + Frobenius + sqrt + compare).
218pub fn bench_von_mises(n: usize) -> MatBenchResult {
219    let mut pts = SoaMaterialPoints::new(n);
220    // Apply a stress state slightly above yield
221    pts.stress_xx.fill(300e6);
222    pts.stress_yy.fill(-50e6);
223    let _f = von_mises_yield_batch(&pts);
224    let flops = 10.0 * n as f64;
225    MatBenchResult {
226        n,
227        mflops: Some(flops / 1e6),
228    }
229}
230
231/// Benchmark Neo-Hookean hyperelastic Cauchy stress.
232///
233/// FLOPs ≈ 50 × n  (det F, left Cauchy-Green, stress assembly).
234pub fn bench_neo_hookean(n: usize) -> MatBenchResult {
235    let mut pts = SoaMaterialPoints::new(n);
236    let e = 210e9_f64;
237    let nu = 0.3_f64;
238    // Perturb F slightly from identity
239    for i in 0..n {
240        pts.f11[i] = 1.001;
241        pts.f22[i] = 0.999;
242    }
243    neo_hookean_stress_batch(&mut pts, e, nu);
244    let flops = 50.0 * n as f64;
245    MatBenchResult {
246        n,
247        mflops: Some(flops / 1e6),
248    }
249}
250
251/// Benchmark Perzyna viscoplastic flow rate.
252///
253/// FLOPs ≈ 12 × n.
254pub fn bench_viscoplastic(n: usize) -> MatBenchResult {
255    let mut pts = SoaMaterialPoints::new(n);
256    pts.stress_xx.fill(400e6); // overstressed
257    // Compute yield values first
258    let yield_vals = von_mises_yield_batch(&pts);
259    let _rates = viscoplastic_rate_batch(&pts, &yield_vals, 1.0, 1e6);
260    let flops = 12.0 * n as f64;
261    MatBenchResult {
262        n,
263        mflops: Some(flops / 1e6),
264    }
265}
266
267/// Benchmark Miner's rule fatigue damage accumulation (Basquin relation).
268///
269/// FLOPs ≈ 5 × n.
270pub fn bench_fatigue(n: usize) -> MatBenchResult {
271    let mut pts = SoaMaterialPoints::new(n);
272    pts.stress_xx.fill(300e6);
273    // miner_damage_batch(pts, sigma_f_prime, b_exponent, n_applied)
274    miner_damage_batch(&mut pts, 500e6, -0.1, 1000.0);
275    let flops = 5.0 * n as f64;
276    MatBenchResult {
277        n,
278        mflops: Some(flops / 1e6),
279    }
280}
281
282/// Benchmark thermo-elastic stress correction.
283///
284/// FLOPs ≈ 8 × n.
285pub fn bench_thermal_expansion(n: usize) -> MatBenchResult {
286    let mut pts = SoaMaterialPoints::new(n);
287    for i in 0..n {
288        pts.temperature[i] = 400.0;
289    } // 400 K (hot)
290    thermal_expansion_stress_batch(&mut pts, 210e9, 0.3, 12e-6, 293.15);
291    let flops = 8.0 * n as f64;
292    MatBenchResult {
293        n,
294        mflops: Some(flops / 1e6),
295    }
296}
297
298/// Benchmark J2 return-mapping (elasto-plastic radial return).
299///
300/// FLOPs ≈ 30 × n.
301pub fn bench_return_mapping(n: usize) -> MatBenchResult {
302    let mut pts = SoaMaterialPoints::new(n);
303    pts.stress_xx.fill(500e6); // overstressed; will be corrected to yield surface
304    // return_mapping_batch(pts, e, nu, h) — h = isotropic hardening modulus
305    return_mapping_batch(&mut pts, 210e9, 0.3, 1e9);
306    let flops = 30.0 * n as f64;
307    MatBenchResult {
308        n,
309        mflops: Some(flops / 1e6),
310    }
311}
312
313// ── Full benchmark suite ──────────────────────────────────────────────────────
314
315/// Run the complete materials benchmark suite and return a formatted summary.
316///
317/// ```
318/// use oxiphysics_materials::materials_bench::run_mat_bench_suite;
319/// let s = run_mat_bench_suite(false);
320/// assert!(!s.is_empty());
321/// ```
322pub fn run_mat_bench_suite(verbose: bool) -> String {
323    let mut h = MatBenchHarness::fast();
324
325    let sizes = [1_000usize, 10_000, 100_000];
326
327    for &n in &sizes {
328        h.run(&format!("elastic_n{}", n), || bench_elastic(n));
329        h.run(&format!("elastic_x4_n{}", n), || bench_elastic_x4(n));
330        h.run(&format!("von_mises_n{}", n), || bench_von_mises(n));
331        h.run(&format!("neo_hookean_n{}", n), || bench_neo_hookean(n));
332        h.run(&format!("viscoplastic_n{}", n), || bench_viscoplastic(n));
333        h.run(&format!("fatigue_n{}", n), || bench_fatigue(n));
334        h.run(&format!("thermal_exp_n{}", n), || {
335            bench_thermal_expansion(n)
336        });
337        h.run(&format!("return_map_n{}", n), || bench_return_mapping(n));
338    }
339
340    let mut out = String::new();
341    if verbose {
342        for r in &h.reports {
343            out.push_str(&format!("{}\n", r));
344        }
345    } else {
346        out = format!("{} material benchmarks completed", h.reports.len());
347    }
348    out
349}
350
351// ── tests ─────────────────────────────────────────────────────────────────────
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn test_bench_elastic_runs() {
359        let r = bench_elastic(100);
360        assert_eq!(r.n, 100);
361        assert!(r.mflops.is_some());
362    }
363
364    #[test]
365    fn test_bench_elastic_x4_runs() {
366        let r = bench_elastic_x4(100);
367        assert_eq!(r.n, 100);
368    }
369
370    #[test]
371    fn test_bench_von_mises_runs() {
372        let r = bench_von_mises(100);
373        assert_eq!(r.n, 100);
374    }
375
376    #[test]
377    fn test_bench_neo_hookean_runs() {
378        let r = bench_neo_hookean(100);
379        assert_eq!(r.n, 100);
380    }
381
382    #[test]
383    fn test_bench_viscoplastic_runs() {
384        let r = bench_viscoplastic(100);
385        assert_eq!(r.n, 100);
386    }
387
388    #[test]
389    fn test_bench_fatigue_runs() {
390        let r = bench_fatigue(100);
391        assert_eq!(r.n, 100);
392    }
393
394    #[test]
395    fn test_bench_thermal_expansion_runs() {
396        let r = bench_thermal_expansion(100);
397        assert_eq!(r.n, 100);
398    }
399
400    #[test]
401    fn test_bench_return_mapping_runs() {
402        let r = bench_return_mapping(100);
403        assert_eq!(r.n, 100);
404    }
405
406    #[test]
407    fn test_harness_collects_reports() {
408        let mut h = MatBenchHarness {
409            warmup: 0,
410            iterations: 2,
411            reports: Vec::new(),
412        };
413        h.run("el_100", || bench_elastic(100));
414        h.run("vm_100", || bench_von_mises(100));
415        assert_eq!(h.reports.len(), 2);
416    }
417
418    #[test]
419    fn test_run_mat_bench_suite_compact() {
420        let s = run_mat_bench_suite(false);
421        assert!(s.contains("material benchmarks"), "output: {}", s);
422    }
423
424    #[test]
425    fn test_run_mat_bench_suite_verbose() {
426        let s = run_mat_bench_suite(true);
427        assert!(s.contains("elastic"), "output: {}", s);
428    }
429}