use std::path::Path;
use crate::Axis;
use crate::engine::{EdgeDft, Grid};
use crate::io::{Layout, VtrWriter};
use crate::post::Complex64;
use crate::{Error, Result};
#[derive(Debug, Clone, PartialEq)]
pub struct SarVolume {
lines: [Vec<f64>; 3],
power: Vec<f64>,
density: Vec<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(u8)]
pub enum VoxelClass {
#[default]
NoTissue = 0,
Local = 1,
Centered = 2,
Covered = 3,
Boundary = 4,
Unresolved = 5,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AveragingOptions {
pub max_air_fraction: f64,
pub threads: usize,
}
impl Default for AveragingOptions {
fn default() -> Self {
Self {
max_air_fraction: 0.1,
threads: 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SarMax {
pub value: f64,
pub cell: [usize; 3],
pub center: [f64; 3],
}
#[derive(Debug, Clone, PartialEq)]
pub struct SarField {
lines: [Vec<f64>; 3],
values: Vec<f64>,
classes: Vec<VoxelClass>,
mass: Option<f64>,
}
fn cells_of(lines: &[Vec<f64>; 3]) -> [usize; 3] {
[0, 1, 2].map(|a| lines[a].len().saturating_sub(1))
}
impl SarVolume {
pub fn new(lines: [Vec<f64>; 3], power: Vec<f64>, density: Vec<f64>) -> Result<Self> {
for (a, l) in lines.iter().enumerate() {
if l.len() < 2
|| l.windows(2).any(|w| w[1] <= w[0] || w[1].is_nan())
|| !l[0].is_finite()
{
return Err(Error::Invalid(format!(
"SAR volume: lines along axis {a} must be at least two increasing values"
)));
}
}
let n: usize = cells_of(&lines).iter().product();
if power.len() != n || density.len() != n {
return Err(Error::Invalid(format!(
"SAR volume: {n} cells but {} power and {} density values",
power.len(),
density.len()
)));
}
if let Some(x) = power
.iter()
.chain(&density)
.find(|x| !(x.is_finite() && **x >= 0.0))
{
return Err(Error::Invalid(format!(
"SAR volume: power and density must be finite and non-negative, got {x}"
)));
}
Ok(Self {
lines,
power,
density,
})
}
pub fn from_edge_fields(
lines: [Vec<f64>; 3],
e: [&[Complex64]; 3],
sigma: &[[f64; 3]],
density: Vec<f64>,
) -> Result<Self> {
let c = cells_of(&lines);
let n: usize = c.iter().product();
if sigma.len() != n {
return Err(Error::Invalid(format!(
"SAR volume: {n} cells but {} conductivities",
sigma.len()
)));
}
if let Some(s) = sigma
.iter()
.flatten()
.find(|s| !(s.is_finite() && **s >= 0.0))
{
return Err(Error::Invalid(format!(
"SAR volume: conductivity must be finite and non-negative, got {s}"
)));
}
let dims =
Axis::ALL.map(|a| [0, 1, 2].map(|b| if b == a.index() { c[b] } else { c[b] + 1 }));
for a in 0..3 {
let want: usize = dims[a].iter().product();
if e[a].len() != want {
return Err(Error::Invalid(format!(
"SAR volume: {} edge values along axis {a}, expected {want}",
e[a].len()
)));
}
}
let mut power = vec![0.0; n];
for i in 0..c[0] {
for j in 0..c[1] {
for k in 0..c[2] {
let cell = (i * c[1] + j) * c[2] + k;
let mut p = 0.0;
for a in 0..3 {
let s = sigma[cell][a];
if s == 0.0 {
continue;
}
let (b, cc) = ((a + 1) % 3, (a + 2) % 3);
let d = dims[a];
let mut sum = 0.0;
for db in 0..2 {
for dc in 0..2 {
let mut n = [i, j, k];
n[b] += db;
n[cc] += dc;
sum += e[a][(n[0] * d[1] + n[1]) * d[2] + n[2]].norm_sqr();
}
}
p += s * 0.25 * sum;
}
power[cell] = 0.5 * p;
}
}
}
Self::new(lines, power, density)
}
pub fn from_edge_dft(
grid: &Grid,
dft: &EdgeDft,
k: usize,
sigma: &[[f64; 3]],
density: Vec<f64>,
) -> Result<Self> {
let b = *dft.cell_box();
let nodes = grid.nodes();
if (0..3).any(|a| b.hi()[a] >= nodes[a]) {
return Err(Error::Invalid(
"SAR volume: the edge DFT box does not fit the grid".into(),
));
}
let f = dft.freqs()[k];
let corr = (core::f64::consts::PI * f * dft.dt()).cos();
let lo = b.lo();
let e = Axis::ALL.map(|axis| {
let a = axis.index();
let d = dft.edge_dims(axis);
let mut u = dft.voltages(axis, k);
for (idx, v) in u.iter_mut().enumerate() {
let along = [idx / (d[1] * d[2]), (idx / d[2]) % d[1], idx % d[2]][a];
*v *= corr / grid.delta(axis, lo[a] + along);
}
u
});
let lines = Axis::ALL.map(|a| b.lines(grid, a).to_vec());
Self::from_edge_fields(lines, [&e[0], &e[1], &e[2]], sigma, density)
}
pub fn lines(&self, axis: Axis) -> &[f64] {
&self.lines[axis.index()]
}
pub fn cells(&self) -> [usize; 3] {
cells_of(&self.lines)
}
pub fn power_density(&self) -> &[f64] {
&self.power
}
pub fn density(&self) -> &[f64] {
&self.density
}
pub fn cell_volumes(&self) -> Vec<f64> {
let c = self.cells();
let d: [Vec<f64>; 3] =
core::array::from_fn(|a| self.lines[a].windows(2).map(|w| w[1] - w[0]).collect());
let mut v = Vec::with_capacity(c.iter().product());
for i in 0..c[0] {
for j in 0..c[1] {
for k in 0..c[2] {
v.push(d[0][i] * d[1][j] * d[2][k]);
}
}
}
v
}
pub fn total_power(&self) -> f64 {
self.power
.iter()
.zip(self.cell_volumes())
.map(|(p, v)| p * v)
.sum()
}
pub fn tissue_power(&self) -> f64 {
self.power
.iter()
.zip(&self.density)
.zip(self.cell_volumes())
.filter(|((_, r), _)| **r > 0.0)
.map(|((p, _), v)| p * v)
.sum()
}
pub fn tissue_mass(&self) -> f64 {
self.density
.iter()
.zip(self.cell_volumes())
.map(|(r, v)| r * v)
.sum()
}
pub fn whole_average(&self) -> f64 {
let m = self.tissue_mass();
if m > 0.0 {
self.tissue_power() / m
} else {
0.0
}
}
pub fn scaled(&self, factor: f64) -> Self {
let mut v = self.clone();
for p in &mut v.power {
*p *= factor;
}
v
}
pub fn local(&self) -> SarField {
let (values, classes) = self
.power
.iter()
.zip(&self.density)
.map(|(&p, &r)| {
if r > 0.0 {
(p / r, VoxelClass::Local)
} else {
(0.0, VoxelClass::NoTissue)
}
})
.unzip();
SarField {
lines: self.lines.clone(),
values,
classes,
mass: None,
}
}
pub fn averaged(&self, mass: f64) -> Result<SarField> {
self.averaged_with(mass, &AveragingOptions::default())
}
pub fn averaged_with(&self, mass: f64, opts: &AveragingOptions) -> Result<SarField> {
if !(mass.is_finite() && mass > 0.0) {
return Err(Error::Invalid(format!(
"SAR averaging mass must be positive, got {mass}"
)));
}
if !(0.0..1.0).contains(&opts.max_air_fraction) {
return Err(Error::Invalid(format!(
"SAR max air fraction must be in [0, 1), got {}",
opts.max_air_fraction
)));
}
Ok(Averager::new(self, mass, opts).run())
}
}
pub fn local_sar(v: &SarVolume) -> SarField {
v.local()
}
pub fn averaged_sar(v: &SarVolume, mass: f64) -> Result<SarField> {
v.averaged(mass)
}
impl SarField {
pub fn lines(&self, axis: Axis) -> &[f64] {
&self.lines[axis.index()]
}
pub fn cells(&self) -> [usize; 3] {
cells_of(&self.lines)
}
pub fn values(&self) -> &[f64] {
&self.values
}
pub fn classes(&self) -> &[VoxelClass] {
&self.classes
}
pub fn mass(&self) -> Option<f64> {
self.mass
}
pub fn get(&self, cell: [usize; 3]) -> f64 {
let c = self.cells();
assert!((0..3).all(|a| cell[a] < c[a]), "cell {cell:?} out of range");
self.values[(cell[0] * c[1] + cell[1]) * c[2] + cell[2]]
}
pub fn max(&self) -> Option<SarMax> {
let c = self.cells();
let mut best: Option<(usize, f64, bool)> = None;
for (i, (&v, &k)) in self.values.iter().zip(&self.classes).enumerate() {
if matches!(k, VoxelClass::NoTissue | VoxelClass::Unresolved) {
continue;
}
let centred = k == VoxelClass::Centered;
if best.is_none_or(|(_, b, bc)| v > b || (v == b && centred && !bc)) {
best = Some((i, v, centred));
}
}
best.map(|(i, value, _)| {
let cell = [i / (c[1] * c[2]), (i / c[2]) % c[1], i % c[2]];
SarMax {
value,
cell,
center: [0, 1, 2]
.map(|a| 0.5 * (self.lines[a][cell[a]] + self.lines[a][cell[a] + 1])),
}
})
}
pub fn write_vtr(&self, path: impl AsRef<Path>) -> Result<()> {
let class: Vec<f32> = self.classes.iter().map(|&c| f32::from(c as u8)).collect();
let mass = [self.mass.unwrap_or(0.0)];
VtrWriter::new(&self.lines[0], &self.lines[1], &self.lines[2])
.field("Mass", &mass)
.cell_scalar("SAR", &self.values, Layout::ZFastest)
.cell_scalar("class", &class, Layout::ZFastest)
.write(path)
}
#[cfg(feature = "hdf5")]
pub fn write_h5(&self, path: impl AsRef<Path>) -> Result<()> {
let mut f = hdf5rs::write::FileWriter::create(path.as_ref()).map_err(h5)?;
self.fill_h5(&mut f).map_err(h5)?;
f.finish().map_err(h5)?;
Ok(())
}
#[cfg(feature = "hdf5")]
pub fn h5_bytes(&self) -> Result<Vec<u8>> {
let mut f = hdf5rs::write::FileWriter::in_memory();
self.fill_h5(&mut f).map_err(h5)?;
f.finish().map_err(h5)
}
#[cfg(feature = "hdf5")]
fn fill_h5<S: hdf5rs::write::WriteAt>(
&self,
f: &mut hdf5rs::write::FileWriter<S>,
) -> hdf5rs::Result<()> {
let mut root = f.root();
root.set_attr("format", "ems-sar")?;
root.set_attr("version", &1u32)?;
root.set_attr("mass", &self.mass.unwrap_or(0.0))?;
root.set_attr("unit", "W/kg")?;
let mut mesh = root.create_group("mesh")?;
for (a, n) in ["x", "y", "z"].into_iter().enumerate() {
let l = &self.lines[a];
mesh.new_dataset::<f64>(n)
.shape(&[l.len() as u64])
.write(l)?
.set_attr("unit", "m")?;
}
let shape = self.cells().map(|n| n as u64);
root.new_dataset::<f64>("SAR")
.shape(&shape)
.write(&self.values)?
.set_attr("unit", "W/kg")?;
let class: Vec<u8> = self.classes.iter().map(|&c| c as u8).collect();
root.new_dataset::<u8>("class")
.shape(&shape)
.write(&class)?;
Ok(())
}
}
#[cfg(feature = "hdf5")]
fn h5(e: hdf5rs::Error) -> Error {
Error::Io(format!("HDF5: {e}"))
}
struct Prefix {
c: [usize; 3],
data: Vec<[f64; 3]>,
}
impl Prefix {
fn new(c: [usize; 3], cell: impl Fn(usize) -> [f64; 3]) -> Self {
let n = [c[0] + 1, c[1] + 1, c[2] + 1];
let idx = |i: usize, j: usize, k: usize| (i * n[1] + j) * n[2] + k;
let mut data = vec![[0.0; 3]; n[0] * n[1] * n[2]];
for i in 1..n[0] {
for j in 1..n[1] {
for k in 1..n[2] {
let v = cell(((i - 1) * c[1] + j - 1) * c[2] + k - 1);
let mut s = [0.0; 3];
for (q, s) in s.iter_mut().enumerate() {
*s = v[q]
+ data[idx(i - 1, j, k)][q]
+ data[idx(i, j - 1, k)][q]
+ data[idx(i, j, k - 1)][q]
- data[idx(i - 1, j - 1, k)][q]
- data[idx(i - 1, j, k - 1)][q]
- data[idx(i, j - 1, k - 1)][q]
+ data[idx(i - 1, j - 1, k - 1)][q];
}
data[idx(i, j, k)] = s;
}
}
}
Self { c, data }
}
fn sum(&self, r: [(usize, usize); 3]) -> [f64; 3] {
let n = [self.c[0] + 1, self.c[1] + 1, self.c[2] + 1];
let at = |i: usize, j: usize, k: usize| &self.data[(i * n[1] + j) * n[2] + k];
let ((i0, i1), (j0, j1), (k0, k1)) = (r[0], r[1], r[2]);
let mut s = [0.0; 3];
for (q, s) in s.iter_mut().enumerate() {
*s = at(i1, j1, k1)[q] - at(i0, j1, k1)[q] - at(i1, j0, k1)[q] - at(i1, j1, k0)[q]
+ at(i0, j0, k1)[q]
+ at(i0, j1, k0)[q]
+ at(i1, j0, k0)[q]
- at(i0, j0, k0)[q];
}
s
}
}
type Segments = ([((usize, usize), f64); 3], usize);
fn segments(lines: &[f64], a: f64, b: f64) -> Segments {
let mut out = [((0, 0), 0.0); 3];
let n = lines.len() - 1;
let (a, b) = (a.max(lines[0]), b.min(lines[n]));
if b <= a || b.is_nan() || a.is_nan() {
return (out, 0);
}
let i0 = lines.partition_point(|&x| x <= a).clamp(1, n) - 1;
let i1 = lines.partition_point(|&x| x < b).clamp(1, n) - 1;
let d = |i: usize| lines[i + 1] - lines[i];
if i0 == i1 {
out[0] = ((i0, i0 + 1), (b - a) / d(i0));
return (out, 1);
}
let mut m = 0;
out[m] = ((i0, i0 + 1), (lines[i0 + 1] - a) / d(i0));
m += 1;
if i1 > i0 + 1 {
out[m] = ((i0 + 1, i1), 1.0);
m += 1;
}
out[m] = ((i1, i1 + 1), (b - lines[i1]) / d(i1));
(out, m + 1)
}
#[derive(Debug, Clone, Copy)]
enum Placement {
Centered,
Face { axis: usize, up: bool },
}
#[derive(Debug, Clone, Copy)]
struct Cube {
side: f64,
sar: f64,
air: f64,
}
struct Averager<'a> {
v: &'a SarVolume,
mass: f64,
opts: AveragingOptions,
c: [usize; 3],
prefix: Prefix,
centers: [Vec<f64>; 3],
widths: [Vec<f64>; 3],
total_mass: f64,
}
impl<'a> Averager<'a> {
fn new(v: &'a SarVolume, mass: f64, opts: &AveragingOptions) -> Self {
let c = v.cells();
let vol = v.cell_volumes();
let prefix = Prefix::new(c, |i| {
if v.density[i] > 0.0 {
[v.density[i] * vol[i], v.power[i] * vol[i], vol[i]]
} else {
[0.0; 3]
}
});
let centers =
core::array::from_fn(|a| v.lines[a].windows(2).map(|w| 0.5 * (w[0] + w[1])).collect());
let widths = core::array::from_fn(|a| v.lines[a].windows(2).map(|w| w[1] - w[0]).collect());
Self {
v,
mass,
opts: *opts,
c,
prefix,
centers,
widths,
total_mass: v.tissue_mass(),
}
}
fn cell(&self, i: usize) -> [usize; 3] {
let c = self.c;
[i / (c[1] * c[2]), (i / c[2]) % c[1], i % c[2]]
}
fn integrate(&self, lo: [f64; 3], hi: [f64; 3]) -> [f64; 3] {
let s: [_; 3] = core::array::from_fn(|a| segments(&self.v.lines[a], lo[a], hi[a]));
let mut out = [0.0; 3];
for &(rx, wx) in &s[0].0[..s[0].1] {
for &(ry, wy) in &s[1].0[..s[1].1] {
for &(rz, wz) in &s[2].0[..s[2].1] {
let w = wx * wy * wz;
let v = self.prefix.sum([rx, ry, rz]);
for q in 0..3 {
out[q] += w * v[q];
}
}
}
}
out
}
fn bounds(&self, cell: [usize; 3], p: Placement, s: f64) -> ([f64; 3], [f64; 3]) {
let mut lo = [0.0; 3];
let mut hi = [0.0; 3];
for a in 0..3 {
let x = self.centers[a][cell[a]];
let h = 0.5 * self.widths[a][cell[a]];
(lo[a], hi[a]) = match p {
Placement::Face { axis, up } if axis == a => {
if up {
(x + h - s, x + h)
} else {
(x - h, x - h + s)
}
}
_ => (x - 0.5 * s, x + 0.5 * s),
};
}
(lo, hi)
}
fn cube(&self, cell: [usize; 3], p: Placement) -> Option<Cube> {
let m = self.mass;
if self.total_mass < m * (1.0 - 1e-12) {
return None;
}
let eval = |s: f64| {
let (lo, hi) = self.bounds(cell, p, s);
self.integrate(lo, hi)
};
let f = |s: f64| eval(s)[0] - m;
let span: f64 = (0..3)
.map(|a| self.v.lines[a][self.c[a]] - self.v.lines[a][0])
.fold(0.0, f64::max);
let s_max = 2.0 * span + 1e-300;
let i = (cell[0] * self.c[1] + cell[1]) * self.c[2] + cell[2];
let rho = self.v.density[i].max(1e-300);
let mut hi = (m / rho).cbrt().min(s_max);
let mut f_hi = f(hi);
while f_hi < 0.0 {
if hi >= s_max {
return None;
}
hi = (2.0 * hi).min(s_max);
f_hi = f(hi);
}
let (mut lo, mut f_lo) = (0.0, -m);
let mut side = 0;
let mut s = hi;
for _ in 0..200 {
s = (lo * f_hi - hi * f_lo) / (f_hi - f_lo);
if !(s > lo && s < hi) {
s = 0.5 * (lo + hi);
}
let fs = f(s);
if fs.abs() <= 1e-12 * m || hi - lo <= 1e-13 * hi {
break;
}
if fs > 0.0 {
hi = s;
f_hi = fs;
if side == 1 {
f_lo *= 0.5;
}
side = 1;
} else {
lo = s;
f_lo = fs;
if side == -1 {
f_hi *= 0.5;
}
side = -1;
}
}
let [mass, power, tissue] = eval(s);
if mass <= 0.0 {
return None;
}
Some(Cube {
side: s,
sar: power / mass,
air: (1.0 - tissue / (s * s * s)).max(0.0),
})
}
fn threads(&self) -> usize {
match self.opts.threads {
0 => std::thread::available_parallelism().map_or(1, usize::from),
n => n,
}
}
fn par_map<T: Send + Clone + Default>(
&self,
n: usize,
f: impl Fn(usize) -> T + Sync,
) -> Vec<T> {
let t = self.threads().clamp(1, n.max(1));
let mut out = vec![T::default(); n];
let chunk = n.div_ceil(t).max(1);
std::thread::scope(|sc| {
for (c, part) in out.chunks_mut(chunk).enumerate() {
let f = &f;
sc.spawn(move || {
for (o, slot) in part.iter_mut().enumerate() {
*slot = f(c * chunk + o);
}
});
}
});
out
}
fn run(&self) -> SarField {
let n: usize = self.c.iter().product();
let tissue = |i: usize| self.v.density[i] > 0.0;
let centred: Vec<Option<(f64, f64)>> = self.par_map(n, |i| {
if !tissue(i) {
return None;
}
self.cube(self.cell(i), Placement::Centered)
.filter(|q| q.air <= self.opts.max_air_fraction)
.map(|q| (q.sar, q.side))
});
let half_max = centred
.iter()
.flatten()
.map(|&(_, s)| 0.5 * s)
.fold(0.0, f64::max);
let rest: Vec<(f64, VoxelClass)> = self.par_map(n, |i| {
if !tissue(i) {
return (0.0, VoxelClass::NoTissue);
}
if let Some((sar, _)) = centred[i] {
return (sar, VoxelClass::Centered);
}
let cell = self.cell(i);
let x = [0, 1, 2].map(|a| self.centers[a][cell[a]]);
let range = |a: usize| {
let c = &self.centers[a];
let lo = c.partition_point(|&y| y < x[a] - half_max);
let hi = c.partition_point(|&y| y <= x[a] + half_max);
lo..hi
};
let mut best: Option<f64> = None;
for p in range(0) {
for q in range(1) {
for r in range(2) {
let j = (p * self.c[1] + q) * self.c[2] + r;
if let Some((sar, side)) = centred[j] {
let h = 0.5 * side;
let inside = (0..3).all(|a| {
let y = self.centers[a][[p, q, r][a]];
(x[a] - y).abs() <= h
});
if inside && best.is_none_or(|b| sar > b) {
best = Some(sar);
}
}
}
}
}
if let Some(b) = best {
return (b, VoxelClass::Covered);
}
let mut pick: Option<Cube> = None;
for axis in 0..3 {
for up in [false, true] {
if let Some(q) = self.cube(cell, Placement::Face { axis, up }) {
let better = match pick {
None => true,
Some(b) => q.air < b.air || (q.air == b.air && q.sar > b.sar),
};
if better {
pick = Some(q);
}
}
}
}
match pick {
Some(q) => (q.sar, VoxelClass::Boundary),
None => (0.0, VoxelClass::Unresolved),
}
});
let (values, classes) = rest.into_iter().unzip();
SarField {
lines: self.v.lines.clone(),
values,
classes,
mass: Some(self.mass),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn uniform(n: usize, d: f64) -> [Vec<f64>; 3] {
[(); 3].map(|_| (0..=n).map(|i| i as f64 * d).collect())
}
#[test]
fn segments_cover_fractions() {
let l = [0.0, 1.0, 3.0, 4.0];
let (s, n) = segments(&l, 0.5, 3.5);
assert_eq!(n, 3);
assert_eq!(s[0], ((0, 1), 0.5));
assert_eq!(s[1], ((1, 2), 1.0));
assert_eq!(s[2], ((2, 3), 0.5));
let (s, n) = segments(&l, 1.5, 2.5);
assert_eq!(n, 1);
assert_eq!(s[0], ((1, 2), 0.5));
let (s, n) = segments(&l, -1.0, 3.0);
assert_eq!(n, 2);
assert_eq!(s[0], ((0, 1), 1.0));
assert_eq!(s[1], ((1, 2), 1.0));
assert_eq!(segments(&l, 5.0, 6.0).1, 0);
}
#[test]
fn constant_field_average_equals_local() {
let n = 12;
let v = SarVolume::new(
uniform(n, 2e-3),
vec![3.0; n * n * n],
vec![1000.0; n * n * n],
)
.unwrap();
let a = v.averaged(1e-3).unwrap();
for (&s, &k) in a.values().iter().zip(a.classes()) {
assert!(matches!(
k,
VoxelClass::Centered | VoxelClass::Covered | VoxelClass::Boundary
));
assert!((s - 3e-3).abs() < 1e-15, "{s}");
}
let c = |i: usize, j: usize, k: usize| a.classes()[(i * n + j) * n + k];
assert_eq!(c(6, 6, 6), VoxelClass::Centered);
assert_eq!(c(0, 6, 6), VoxelClass::Covered);
assert_eq!(c(0, 0, 0), VoxelClass::Covered);
}
#[test]
fn linear_gradient_average_is_centre_value() {
let n = 30;
let d = 1e-3;
let lines = uniform(n, d);
let mut p = Vec::new();
for i in 0..n {
for _ in 0..n * n {
p.push(1.0 + 1000.0 * (i as f64 + 0.5) * d);
}
}
let v = SarVolume::new(lines, p.clone(), vec![1000.0; n * n * n]).unwrap();
let a = v.averaged(1e-3).unwrap(); let i = (15 * n + 15) * n + 15;
assert_eq!(a.classes()[i], VoxelClass::Centered);
let want = p[i] / 1000.0;
assert!(
(a.values()[i] / want - 1.0).abs() < 1e-12,
"{} {want}",
a.values()[i]
);
let a = v.averaged(1.5e-3).unwrap();
assert!((a.values()[i] / want - 1.0).abs() < 1e-11);
let hot = (29 * n + 15) * n + 15;
assert_eq!(a.classes()[hot], VoxelClass::Covered);
assert!(a.values()[hot] > a.values()[i]);
}
#[test]
fn tissue_boundary_and_air_rule() {
let n = 20;
let d = 1e-3;
let mut rho = vec![0.0; n * n * n];
let mut p = vec![0.0; n * n * n];
rho[..10 * n * n].fill(1000.0);
p[..10 * n * n].fill(2.0);
p[15 * n * n] = 1e6;
let v = SarVolume::new(uniform(n, d), p, rho).unwrap();
assert!((v.tissue_mass() - 1000.0 * 10.0 * 20.0 * 20.0 * 1e-9).abs() < 1e-15);
assert!(v.total_power() > v.tissue_power());
let a = v.averaged(0.125e-3).unwrap(); let at = |i: usize| a.classes()[(i * n + 10) * n + 10];
assert_eq!(at(5), VoxelClass::Centered);
assert_eq!(at(9), VoxelClass::Covered);
assert_eq!(at(15), VoxelClass::NoTissue);
assert!(
a.values()
.iter()
.zip(a.classes())
.all(|(&s, &k)| { k == VoxelClass::NoTissue || (s - 2e-3).abs() < 1e-15 })
);
let mut rho = vec![0.0; n * n * n];
rho[..n * n].fill(1000.0);
let v = SarVolume::new(uniform(n, d), vec![1.0; n * n * n], rho).unwrap();
let a = v.averaged(0.125e-3).unwrap();
let k = a.classes()[10 * n + 10];
assert_eq!(k, VoxelClass::Boundary);
assert!((a.values()[10 * n + 10] - 1e-3).abs() < 1e-15);
let a = v.averaged(1.0).unwrap();
assert!(
a.classes()
.iter()
.all(|&k| matches!(k, VoxelClass::Unresolved | VoxelClass::NoTissue))
);
assert!(a.max().is_none());
}
#[test]
fn non_uniform_mesh_mass_and_threads() {
let l: Vec<f64> = (0..=16).map(|i| (i as f64).powf(1.3) * 1e-3).collect();
let lines = [l.clone(), l.clone(), l];
let n = 16 * 16 * 16;
let v = SarVolume::new(lines, vec![5.0; n], vec![1050.0; n]).unwrap();
let one = v
.averaged_with(
1e-3,
&AveragingOptions {
threads: 1,
..Default::default()
},
)
.unwrap();
let many = v
.averaged_with(
1e-3,
&AveragingOptions {
threads: 7,
..Default::default()
},
)
.unwrap();
assert_eq!(one, many);
for &s in one.values() {
assert!((s - 5.0 / 1050.0).abs() < 1e-14);
}
}
#[test]
fn local_and_edge_power() {
let lines = [vec![0.0, 1e-3], vec![0.0, 2e-3], vec![0.0, 4e-3]];
let one = Complex64::new(1.0, 0.0);
let ex = vec![one; 4];
let ey = vec![
Complex64::new(0.0, 2.0),
Complex64::ZERO,
Complex64::new(2.0, 0.0),
Complex64::ZERO,
];
let ez = vec![Complex64::ZERO; 4];
let v =
SarVolume::from_edge_fields(lines, [&ex, &ey, &ez], &[[1.0, 2.0, 3.0]], vec![500.0])
.unwrap();
assert!((v.power_density()[0] - 2.5).abs() < 1e-15);
assert!((v.total_power() - 2.5 * 8e-9).abs() < 1e-22);
let l = v.local();
assert_eq!(l.values(), &[2.5 / 500.0]);
let m = l.max().unwrap();
assert_eq!(m.cell, [0, 0, 0]);
assert_eq!(m.center, [0.5e-3, 1e-3, 2e-3]);
assert!((v.scaled(2.0).power_density()[0] - 5.0).abs() < 1e-15);
assert!(
SarVolume::from_edge_fields(
[vec![0.0, 1.0], vec![0.0, 1.0], vec![0.0, 1.0]],
[&ex, &ey, &ez[..3]],
&[[1.0; 3]],
vec![1.0]
)
.is_err()
);
}
#[test]
fn invalid_inputs() {
let l = || [vec![0.0, 1.0], vec![0.0, 1.0], vec![0.0, 1.0]];
assert!(SarVolume::new(l(), vec![-1.0], vec![1.0]).is_err());
assert!(SarVolume::new(l(), vec![1.0, 2.0], vec![1.0]).is_err());
assert!(
SarVolume::new(
[vec![0.0, 0.0], vec![0.0, 1.0], vec![0.0, 1.0]],
vec![1.0],
vec![1.0]
)
.is_err()
);
let v = SarVolume::new(l(), vec![1.0], vec![1.0]).unwrap();
assert!(v.averaged(0.0).is_err());
assert!(
v.averaged_with(
1.0,
&AveragingOptions {
max_air_fraction: 1.0,
threads: 0
}
)
.is_err()
);
}
}