use glam::DVec3;
use pantometry_core::conserved::quantity;
use pantometry_core::{Domain, Exchange, Kind, Ledger, Reading, ScalarField, Violation};
use pantometry_units::{
Conductivity, Current, CurrentDensity, Energy, Length, LengthVec, Power, Resistance,
Resistivity, Time, Voltage,
};
use crate::HEAT;
const ITERATION_BUDGET: usize = 4;
#[derive(Clone, Debug)]
pub struct Conductor {
name: String,
counts: (usize, usize, usize),
dx: f64,
sigma: Vec<f64>,
phi: Vec<f64>,
drive: f64,
residual: f64,
converged: bool,
dissipated: f64,
tolerance: f64,
max_iterations: Option<usize>,
}
impl Conductor {
pub fn new(
name: impl Into<String>,
counts: (usize, usize, usize),
dx: Length,
material: Resistivity,
drive: Voltage,
) -> Conductor {
let counts = (counts.0.max(1), counts.1.max(1), counts.2.max(1));
let cells = counts.0 * counts.1 * counts.2;
let sigma = material.conductivity().to_si();
let mut built = Conductor {
name: name.into(),
counts,
dx: dx.to_si(),
sigma: vec![sigma; cells],
phi: vec![0.0; cells],
drive: drive.to_si(),
residual: f64::INFINITY,
converged: false,
dissipated: 0.0,
tolerance: 1e-12,
max_iterations: None,
};
built.solve(built.tolerance);
built
}
pub fn with_solver(mut self, tolerance: f64, max_iterations: usize) -> Conductor {
self.tolerance = tolerance;
self.max_iterations = Some(max_iterations);
self
}
pub fn counts(&self) -> (usize, usize, usize) {
self.counts
}
pub fn spacing(&self) -> Length {
Length::from_si(self.dx)
}
pub fn size(&self) -> LengthVec {
let (nx, ny, nz) = self.counts;
LengthVec::from_si(DVec3::new(nx as f64, ny as f64, nz as f64) * self.dx)
}
pub fn section(&self) -> pantometry_units::Area {
let (_, ny, nz) = self.counts;
pantometry_units::Area::from_si((ny * nz) as f64 * self.dx * self.dx)
}
pub fn drive(&self) -> Voltage {
Voltage::from_si(self.drive)
}
pub fn set_resistivity(&mut self, i: usize, j: usize, k: usize, material: Resistivity) {
if let Some(idx) = self.index(i, j, k) {
self.sigma[idx] = material.conductivity().to_si();
self.converged = false;
self.residual = f64::INFINITY;
}
}
pub fn set_region(
&mut self,
mut which: impl FnMut(usize, usize, usize) -> bool,
material: Resistivity,
) {
let (nx, ny, nz) = self.counts;
let sigma = material.conductivity().to_si();
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
if which(i, j, k) {
self.sigma[i + nx * (j + ny * k)] = sigma;
}
}
}
}
self.converged = false;
self.residual = f64::INFINITY;
}
pub fn index(&self, i: usize, j: usize, k: usize) -> Option<usize> {
let (nx, ny, nz) = self.counts;
(i < nx && j < ny && k < nz).then(|| i + nx * (j + ny * k))
}
pub fn potential_at(&self, i: usize, j: usize, k: usize) -> Voltage {
let (nx, ny, nz) = self.counts;
let idx = self
.index(i.min(nx - 1), j.min(ny - 1), k.min(nz - 1))
.expect("clamped indices are in range");
Voltage::from_si(self.phi[idx])
}
pub fn current_density_at(&self, i: usize, j: usize, k: usize) -> DVec3 {
let (nx, ny, nz) = self.counts;
let (i, j, k) = (i.min(nx - 1), j.min(ny - 1), k.min(nz - 1));
let here = self.index(i, j, k).expect("clamped");
let axis = |lo: Option<usize>, hi: Option<usize>| -> f64 {
match (lo, hi) {
(Some(a), Some(b)) => (self.phi[b] - self.phi[a]) / (2.0 * self.dx),
(None, Some(b)) => (self.phi[b] - self.phi[here]) / self.dx,
(Some(a), None) => (self.phi[here] - self.phi[a]) / self.dx,
(None, None) => 0.0,
}
};
let grad = DVec3::new(
axis(
i.checked_sub(1).and_then(|a| self.index(a, j, k)),
self.index(i + 1, j, k),
),
axis(
j.checked_sub(1).and_then(|b| self.index(i, b, k)),
self.index(i, j + 1, k),
),
axis(
k.checked_sub(1).and_then(|c| self.index(i, j, c)),
self.index(i, j, k + 1),
),
);
-self.sigma[here] * grad
}
pub fn current(&self) -> Current {
Current::from_si(self.electrode_current(true))
}
pub fn current_balance(&self) -> f64 {
let (a, b) = (self.electrode_current(true), -self.electrode_current(false));
let scale = a.abs().max(b.abs());
if scale <= 0.0 {
0.0
} else {
(a - b).abs() / scale
}
}
pub fn resistance(&self) -> Resistance {
let i = self.electrode_current(true);
if i.abs() <= 0.0 {
return Resistance::from_si(f64::INFINITY);
}
Resistance::from_si(self.drive / i)
}
pub fn dissipation(&self) -> Power {
let mut total = 0.0;
for (a, b, g) in self.faces() {
let dphi = self.phi_of(b) - self.phi_of(a);
total += g * dphi * dphi;
}
Power::from_si(total)
}
pub fn dissipated_energy(&self) -> Energy {
Energy::from_si(self.dissipated)
}
pub fn converged(&self) -> bool {
self.converged
}
pub fn residual(&self) -> f64 {
self.residual
}
pub fn solve(&mut self, tolerance: f64) -> bool {
let budget = self
.max_iterations
.unwrap_or(ITERATION_BUDGET * self.phi.len() + 32);
self.solve_within(tolerance, budget)
}
pub fn solve_within(&mut self, tolerance: f64, max_iterations: usize) -> bool {
let n = self.phi.len();
let budget = max_iterations;
let b = self.source();
let mut x = std::mem::take(&mut self.phi);
if x.len() != n {
x = vec![0.0; n];
}
let mut r = b.clone();
let ax = self.apply(&x);
for (ri, axi) in r.iter_mut().zip(&ax) {
*ri -= axi;
}
let mut p = r.clone();
let mut rr: f64 = r.iter().map(|v| v * v).sum();
let scale: f64 = b
.iter()
.map(|v| v * v)
.sum::<f64>()
.sqrt()
.max(f64::MIN_POSITIVE);
let mut iterations = 0;
while rr.sqrt() / scale > tolerance && iterations < budget {
let ap = self.apply(&p);
let pap: f64 = p.iter().zip(&ap).map(|(a, b)| a * b).sum();
if pap <= 0.0 {
break;
}
let alpha = rr / pap;
for (xi, pi) in x.iter_mut().zip(&p) {
*xi += alpha * pi;
}
for (ri, api) in r.iter_mut().zip(&ap) {
*ri -= alpha * api;
}
let rr_next: f64 = r.iter().map(|v| v * v).sum();
let beta = rr_next / rr;
for (pi, ri) in p.iter_mut().zip(&r) {
*pi = ri + beta * *pi;
}
rr = rr_next;
iterations += 1;
}
self.phi = x;
self.residual = rr.sqrt() / scale;
self.converged = self.residual <= tolerance;
self.converged
}
fn faces(&self) -> Vec<(Side, Side, f64)> {
let (nx, ny, nz) = self.counts;
let area = self.dx * self.dx;
let mut out = Vec::new();
for k in 0..nz {
for j in 0..ny {
let low = i_index(0, j, k, nx, ny);
out.push((
Side::Electrode(false),
Side::Cell(low),
self.sigma[low] * area / (0.5 * self.dx),
));
let high = i_index(nx - 1, j, k, nx, ny);
out.push((
Side::Cell(high),
Side::Electrode(true),
self.sigma[high] * area / (0.5 * self.dx),
));
}
}
let mut interior = |a: usize, b: usize| {
let (sa, sb) = (self.sigma[a], self.sigma[b]);
let g = if sa <= 0.0 || sb <= 0.0 {
0.0
} else {
area / (0.5 * self.dx / sa + 0.5 * self.dx / sb)
};
out.push((Side::Cell(a), Side::Cell(b), g));
};
for k in 0..nz {
for j in 0..ny {
for i in 0..nx - 1 {
interior(i_index(i, j, k, nx, ny), i_index(i + 1, j, k, nx, ny));
}
}
}
for k in 0..nz {
for j in 0..ny - 1 {
for i in 0..nx {
interior(i_index(i, j, k, nx, ny), i_index(i, j + 1, k, nx, ny));
}
}
}
for k in 0..nz - 1 {
for j in 0..ny {
for i in 0..nx {
interior(i_index(i, j, k, nx, ny), i_index(i, j, k + 1, nx, ny));
}
}
}
out
}
fn phi_of(&self, s: Side) -> f64 {
match s {
Side::Cell(i) => self.phi[i],
Side::Electrode(high) => {
if high {
self.drive
} else {
0.0
}
}
}
}
fn apply(&self, x: &[f64]) -> Vec<f64> {
let mut y = vec![0.0; x.len()];
for (a, b, g) in self.faces() {
match (a, b) {
(Side::Cell(i), Side::Cell(j)) => {
let d = g * (x[i] - x[j]);
y[i] += d;
y[j] -= d;
}
(Side::Cell(i), Side::Electrode(_)) | (Side::Electrode(_), Side::Cell(i)) => {
y[i] += g * x[i];
}
(Side::Electrode(_), Side::Electrode(_)) => {}
}
}
y
}
fn source(&self) -> Vec<f64> {
let mut b = vec![0.0; self.phi.len()];
for (a, c, g) in self.faces() {
match (a, c) {
(Side::Electrode(high), Side::Cell(i)) | (Side::Cell(i), Side::Electrode(high)) => {
b[i] += g * if high { self.drive } else { 0.0 };
}
_ => {}
}
}
b
}
fn electrode_current(&self, high: bool) -> f64 {
let phi_e = if high { self.drive } else { 0.0 };
let mut total = 0.0;
for (a, b, g) in self.faces() {
let cell = match (a, b) {
(Side::Electrode(h), Side::Cell(i)) | (Side::Cell(i), Side::Electrode(h))
if h == high =>
{
Some(i)
}
_ => None,
};
if let Some(i) = cell {
total += g * (phi_e - self.phi[i]);
}
}
total
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Side {
Cell(usize),
Electrode(bool),
}
fn i_index(i: usize, j: usize, k: usize, nx: usize, ny: usize) -> usize {
i + nx * (j + ny * k)
}
impl Domain for Conductor {
fn books_balance(&self) -> bool {
true
}
fn name(&self) -> &str {
&self.name
}
fn kind(&self) -> Kind {
Kind::QuasiStatic
}
fn step(&mut self, _t: Time, dt: Time, bus: &mut Exchange) -> Result<(), Violation> {
let want = self.tolerance;
if !self.solve(want) {
return Err(Violation {
quantity: "solver residual".to_string(),
site: format!("{} (conjugate gradients did not converge)", self.name),
before: want,
after: self.residual,
scale: 1.0,
tolerance: want,
});
}
let joules = self.dissipation().to_si() * dt.to_si();
self.dissipated += joules;
bus.publish(HEAT, joules);
Ok(())
}
fn ledger(&self) -> Ledger {
Ledger::new().with(quantity::ENERGY, -self.dissipated)
}
fn readings(&self) -> Vec<Reading> {
vec![
Reading::new(&self.name, "resistance", self.resistance().to_si(), "ohm"),
Reading::new(&self.name, "current", self.current().to_si(), "A"),
Reading::new(&self.name, "dissipating", self.dissipation().to_si(), "W"),
Reading::new(&self.name, "spent", self.dissipated, "J"),
Reading::new(&self.name, "residual", self.residual, ""),
]
}
fn as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
fn as_field(&self) -> Option<&dyn ScalarField> {
Some(self)
}
}
impl ScalarField for Conductor {
fn unit(&self) -> &'static str {
"V"
}
fn at(&self, p: LengthVec, _t: Time) -> f64 {
let (nx, ny, nz) = self.counts;
let q = p.to_si() / self.dx - DVec3::splat(0.5);
if q.is_nan() {
return self.phi[0];
}
let axis = |v: f64, n: usize| -> (usize, f64) {
let last = n.saturating_sub(1);
if v <= 0.0 {
(0, 0.0)
} else if v >= last as f64 {
(last, 0.0)
} else {
let i = v.floor();
(i as usize, v - i)
}
};
let (i, fx) = axis(q.x, nx);
let (j, fy) = axis(q.y, ny);
let (k, fz) = axis(q.z, nz);
let (i1, j1, k1) = (
(i + 1).min(nx - 1),
(j + 1).min(ny - 1),
(k + 1).min(nz - 1),
);
let g = |a: usize, b: usize, c: usize| self.phi[i_index(a, b, c, nx, ny)];
let lerp = |lo: f64, hi: f64, t: f64| lo * (1.0 - t) + hi * t;
let z0 = lerp(
lerp(g(i, j, k), g(i1, j, k), fx),
lerp(g(i, j1, k), g(i1, j1, k), fx),
fy,
);
let z1 = lerp(
lerp(g(i, j, k1), g(i1, j, k1), fx),
lerp(g(i, j1, k1), g(i1, j1, k1), fx),
fy,
);
lerp(z0, z1, fz)
}
fn gradient(&self, p: LengthVec, t: Time, h: Length) -> DVec3 {
let d = h.to_si().max(self.dx);
let sample = |o: DVec3| self.at(LengthVec::from_si(p.to_si() + o), t);
DVec3::new(
(sample(DVec3::X * d) - sample(-DVec3::X * d)) / (2.0 * d),
(sample(DVec3::Y * d) - sample(-DVec3::Y * d)) / (2.0 * d),
(sample(DVec3::Z * d) - sample(-DVec3::Z * d)) / (2.0 * d),
)
}
fn rate(&self, _p: LengthVec, _t: Time, _dt: Time) -> f64 {
0.0
}
}
impl Conductor {
pub fn current_density_magnitude(&self, i: usize, j: usize, k: usize) -> CurrentDensity {
CurrentDensity::from_si(self.current_density_at(i, j, k).length())
}
pub fn conductivity_at(&self, i: usize, j: usize, k: usize) -> Conductivity {
let (nx, ny, nz) = self.counts;
let idx = self
.index(i.min(nx - 1), j.min(ny - 1), k.min(nz - 1))
.expect("clamped");
Conductivity::from_si(self.sigma[idx])
}
}