use petir::mathf::RealMath;
use uom::si::area::square_meter;
use uom::si::f64::{
Area, HeatTransfer, Length, ThermalConductivity, ThermodynamicTemperature,
VolumetricPowerDensity,
};
use uom::si::heat_transfer::watt_per_square_meter_kelvin;
use uom::si::length::meter;
use uom::si::thermal_conductivity::watt_per_meter_kelvin;
use uom::si::thermodynamic_temperature::kelvin;
use uom::si::volumetric_power_density::watt_per_cubic_meter;
use super::*;
fn m(x: f64) -> Length {
Length::new::<meter>(x)
}
fn kw(x: f64) -> ThermalConductivity {
ThermalConductivity::new::<watt_per_meter_kelvin>(x)
}
fn q(x: f64) -> VolumetricPowerDensity {
VolumetricPowerDensity::new::<watt_per_cubic_meter>(x)
}
fn tk(x: f64) -> ThermodynamicTemperature {
ThermodynamicTemperature::new::<kelvin>(x)
}
fn h(x: f64) -> HeatTransfer {
HeatTransfer::new::<watt_per_square_meter_kelvin>(x)
}
fn geometries() -> [RadialGeometry; 3] {
[
RadialGeometry::Slab {
area: Area::new::<square_meter>(0.01),
},
RadialGeometry::Cylinder { height: m(0.5) },
RadialGeometry::Sphere,
]
}
fn two_layers() -> [RadialLayer; 2] {
[
RadialLayer {
outer_radius: m(0.01),
conductivity: kw(3.0),
heat_source: q(3.0e8),
},
RadialLayer {
outer_radius: m(0.015),
conductivity: kw(20.0),
heat_source: q(0.0),
},
]
}
fn convective() -> ThermalBoundary {
ThermalBoundary::Convective {
coefficient: h(5.0e3),
ambient: tk(560.0),
}
}
fn radial_error(
geometry: RadialGeometry,
layers: &[RadialLayer],
surface: ThermalBoundary,
cells_per_layer: usize,
) -> (f64, f64, SteadyConductionSolution) {
let shells: Vec<RadialShell> = layers
.iter()
.map(|l| RadialShell {
outer_radius: l.outer_radius,
cells: cells_per_layer,
})
.collect();
let mesh = radial_mesh(geometry, &shells).expect("mesh");
let problem = SteadyConductionProblem::new(
mesh.clone(),
layers.iter().map(|l| l.heat_source).collect(),
vec![
PatchCondition::uniform(surface),
PatchCondition::uniform(ThermalBoundary::Adiabatic),
],
)
.expect("problem");
let k: Vec<ThermalConductivity> = layers.iter().map(|l| l.conductivity).collect();
let sol = problem
.solve(|z, _| k[z], &SteadyConductionSettings::default())
.expect("solve");
let exact = RadialClosedForm::new(geometry, layers, surface).expect("closed form");
let err = (0..sol.n_cells())
.map(|c| {
let r = m(mesh.mesh.cell_centres[c].x);
(sol.temperature(c).get::<kelvin>() - exact.temperature_at(r).unwrap().get::<kelvin>())
.abs()
})
.fold(0.0_f64, f64::max);
(err, sol.energy_balance.relative_imbalance(), sol)
}
#[test]
fn vv_single_layer_error_is_the_predicted_uniform_offset() {
let layer = RadialLayer {
outer_radius: m(0.01),
conductivity: kw(3.0),
heat_source: q(3.0e8),
};
let surface = ThermalBoundary::FixedTemperature(tk(600.0));
for geometry in geometries() {
for n in [5, 10, 20, 40] {
let (_, imbalance, sol) = radial_error(geometry, &[layer], surface, n);
let exact = RadialClosedForm::new(geometry, &[layer], surface).unwrap();
let dr = 0.01 / n as f64;
let mf = f64::from(geometry.exponent());
let offset = 3.0e8 * dr * dr / (8.0 * (mf + 1.0) * 3.0);
let mesh = radial_mesh(
geometry,
&[RadialShell {
outer_radius: m(0.01),
cells: n,
}],
)
.unwrap();
for c in 0..n {
let r = m(mesh.mesh.cell_centres[c].x);
let e = sol.temperature(c).get::<kelvin>()
- exact.temperature_at(r).unwrap().get::<kelvin>();
assert!(
(e - offset).abs() < 1e-9,
"{geometry:?} N={n} cell {c}: error {e} K, predicted offset {offset} K"
);
}
assert!(
imbalance < 1e-9,
"{geometry:?} N={n}: imbalance {imbalance}"
);
}
}
}
#[test]
fn vv_nested_shells_converge_at_second_order() {
for geometry in geometries() {
let mut errors = Vec::new();
for n in [4, 8, 16, 32, 64] {
let (err, imbalance, _) = radial_error(geometry, &two_layers(), convective(), n);
assert!(
imbalance < 1e-9,
"{geometry:?} N={n}: imbalance {imbalance}"
);
errors.push((n, err));
}
for w in errors.windows(2) {
let p = (w[0].1 / w[1].1).r_log2();
println!(
"{geometry:?}: N={} err={:.4e} K -> N={} err={:.4e} K, p={p:.3}",
w[0].0, w[0].1, w[1].0, w[1].1
);
}
let last = &errors[errors.len() - 2..];
let p = (last[0].1 / last[1].1).r_log2();
assert!((1.9..=2.1).contains(&p), "{geometry:?}: observed order {p}");
}
}
#[test]
fn vv_picard_on_linear_conductivity_matches_kirchhoff_solution() {
let (a, b, ts, qv, r_out) = (1.0, 0.004, 600.0, 3.0e8, 0.01);
let exact = |r: f64| {
let rhs = 0.5 * b * ts * ts + a * ts + qv * (r_out * r_out - r * r) / 6.0;
(-a + (a * a + 2.0 * b * rhs).sqrt()) / b
};
let mut errors = Vec::new();
for n in [10, 20, 40, 80, 160, 320] {
let mesh = radial_mesh(
RadialGeometry::Sphere,
&[RadialShell {
outer_radius: m(r_out),
cells: n,
}],
)
.unwrap();
let problem = SteadyConductionProblem::new(
mesh.clone(),
vec![q(qv)],
vec![
PatchCondition::uniform(ThermalBoundary::FixedTemperature(tk(ts))),
PatchCondition::uniform(ThermalBoundary::Adiabatic),
],
)
.unwrap();
let sol = problem
.solve(
|_, t| kw(a + b * t.get::<kelvin>()),
&SteadyConductionSettings::default(),
)
.unwrap();
let err = (0..n)
.map(|c| {
(sol.temperature(c).get::<kelvin>() - exact(mesh.mesh.cell_centres[c].x)).abs()
})
.fold(0.0_f64, f64::max);
println!(
"Picard k(T): N={n} err={err:.4e} K passes={} peak={:.2} K",
sol.picard_passes,
sol.peak_temperature().1.get::<kelvin>()
);
assert!(sol.energy_balance.relative_imbalance() < 1e-9);
assert!(
sol.picard_passes < 60,
"Picard took {} passes",
sol.picard_passes
);
errors.push(err);
}
let p = (errors[errors.len() - 2] / errors[errors.len() - 1]).r_log2();
assert!((1.9..=2.1).contains(&p), "observed order {p}");
}
fn voxel_sphere(cells_across: usize, film_correction: bool) -> (f64, f64, f64) {
let r = 0.01;
let hh = 2.0 * r / cells_across as f64;
let grid = VoxelGrid {
origin: [m(-r - hh); 3],
spacing: m(hh),
counts: [cells_across + 2; 3],
};
let vm = voxel_mesh(&grid, |p| {
let d2: f64 = p.iter().map(|x| x.get::<meter>().r_powi(2)).sum();
(d2 <= r * r).then_some(0)
})
.unwrap();
let mesh = vm.conduction.mesh.clone();
let surface = convective();
let condition = if film_correction {
let fraction = (0..vm.n_surface_faces())
.map(|i| {
let f = mesh.patches[0].start + i;
let c = mesh.face_centres[f];
let n_true = [c.x, c.y, c.z];
let norm = n_true.iter().map(|v| v * v).sum::<f64>().sqrt();
let n_axis = vm.surface_normal[i];
(0..3)
.map(|a| n_axis[a] * n_true[a] / norm)
.sum::<f64>()
.abs()
})
.collect();
PatchCondition::with_film_area_fraction(surface, fraction)
} else {
PatchCondition::uniform(surface)
};
let qv = 3.0e8;
let exact_volume = 4.0 / 3.0 * std::f64::consts::PI * r * r * r;
let voxel_volume: f64 = mesh.cell_volumes.iter().sum();
let problem = SteadyConductionProblem::new(
vm.conduction.clone(),
vec![q(qv * exact_volume / voxel_volume)],
vec![condition],
)
.unwrap();
let sol = problem
.solve(|_, _| kw(3.0), &SteadyConductionSettings::default())
.unwrap();
let exact = RadialClosedForm::new(
RadialGeometry::Sphere,
&[RadialLayer {
outer_radius: m(r),
conductivity: kw(3.0),
heat_source: q(qv),
}],
surface,
)
.unwrap();
let exact_mean = exact.surface_temperature().get::<kelvin>() + qv * r * r / (15.0 * 3.0);
let mean = sol.volume_mean_temperature(&[]).unwrap().get::<kelvin>();
let surf = sol.surface_mean_temperature(0).unwrap().get::<kelvin>();
(
(mean - exact_mean).abs(),
(surf - exact.surface_temperature().get::<kelvin>()).abs(),
sol.energy_balance.relative_imbalance(),
)
}
#[test]
fn vv_voxel_sphere_converges_only_with_the_film_area_correction() {
let mut with = Vec::new();
for n in [8, 16, 32, 64] {
let (mean_err, surf_err, imbalance) = voxel_sphere(n, true);
let (mean_raw, surf_raw, _) = voxel_sphere(n, false);
println!(
"voxel sphere N={n}: corrected mean err {mean_err:.3} K, surface err {surf_err:.3} K | \
uncorrected mean err {mean_raw:.3} K, surface err {surf_raw:.3} K | imbalance {imbalance:.2e}"
);
assert!(imbalance < 1e-9);
with.push((mean_err, surf_err, surf_raw));
}
let last = with[with.len() - 1];
assert!(last.1 < 6.0, "corrected surface error {} K", last.1);
assert!(last.0 < 10.0, "corrected mean error {} K", last.0);
assert!(last.2 > 40.0, "uncorrected surface error {} K", last.2);
assert!(
with[3].1 < with[1].1,
"corrected surface error does not fall"
);
}
#[test]
fn bad_inputs_are_errors() {
let mesh = radial_mesh(
RadialGeometry::Sphere,
&[RadialShell {
outer_radius: m(0.01),
cells: 4,
}],
)
.unwrap();
let adiabatic = PatchCondition::uniform(ThermalBoundary::Adiabatic);
assert!(SteadyConductionProblem::new(
mesh.clone(),
vec![q(1.0)],
vec![adiabatic.clone(), adiabatic.clone()]
)
.is_err());
assert!(SteadyConductionProblem::new(
mesh.clone(),
vec![],
vec![adiabatic.clone(), adiabatic.clone()]
)
.is_err());
assert!(
SteadyConductionProblem::new(mesh.clone(), vec![q(1.0)], vec![adiabatic.clone()]).is_err()
);
let bad_film = PatchCondition::with_film_area_fraction(convective(), vec![0.5, 0.5]);
assert!(SteadyConductionProblem::new(
mesh.clone(),
vec![q(1.0)],
vec![bad_film, adiabatic.clone()]
)
.is_err());
let ok = SteadyConductionProblem::new(
mesh.clone(),
vec![q(1.0)],
vec![PatchCondition::uniform(convective()), adiabatic],
)
.unwrap();
assert!(matches!(
ok.solve(|_, _| kw(0.0), &SteadyConductionSettings::default()),
Err(SteadyConductionError::BadConductivity { .. })
));
assert!(radial_mesh(RadialGeometry::Sphere, &[]).is_err());
assert!(radial_mesh(
RadialGeometry::Sphere,
&[RadialShell {
outer_radius: m(0.01),
cells: 0
}]
)
.is_err());
let grid = VoxelGrid {
origin: [m(0.0); 3],
spacing: m(0.1),
counts: [2, 2, 2],
};
assert!(voxel_mesh(&grid, |_| None).is_err());
let vm = voxel_mesh(&grid, |_| Some(0)).unwrap();
assert_eq!(vm.cell_at([m(0.15), m(0.05), m(0.05)]), Some(1));
assert_eq!(vm.cell_at([m(0.25), m(0.05), m(0.05)]), None);
assert_eq!(vm.n_surface_faces(), 24);
let layer = two_layers();
assert!(
RadialClosedForm::new(RadialGeometry::Sphere, &layer, ThermalBoundary::Adiabatic).is_err()
);
let cf = RadialClosedForm::new(RadialGeometry::Sphere, &layer, convective()).unwrap();
assert!(cf.temperature_at(m(0.02)).is_err());
assert!(cf.centre_temperature() > cf.surface_temperature());
}