use std::time::Instant;
use nalgebra::{ComplexField, DMatrix};
use num_complex::Complex;
use rayon::prelude::*;
use crate::filament::Filament;
use crate::geometry::Geometry;
use crate::gmres::{gmres, GmresOutcome, GmresParams};
use crate::inductance::{mutual_inductance, self_inductance, KernelError};
use crate::mesh::{MeshMatrix, Port};
use crate::pfft::{PfftOperator, PfftParams, PfftStats};
use crate::result::{Counts, Provenance, SweepResult, Timing};
use crate::solve::{
check_frequency, discretize_geometry, filament_resistance, Discretization, SolveError,
SWEEP_MEMORY_BUDGET_BYTES,
};
type C64 = Complex<f64>;
pub const EXACT_DIAGONAL_MAX_LOOP_LENGTH: usize = 32;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct IterativeParams {
pub pfft: PfftParams,
pub gmres: GmresParams,
}
#[derive(Clone, Debug, PartialEq)]
pub struct IterativeSolution {
pub impedance: DMatrix<C64>,
pub gmres: Vec<GmresOutcome>,
}
#[derive(Debug)]
pub struct IterativeSystem {
ports: Vec<Port>,
filaments: Vec<Filament>,
resistances: Vec<f64>,
mesh: MeshMatrix,
operator: PfftOperator,
gmres: GmresParams,
r_ep: DMatrix<f64>,
l_ep: DMatrix<f64>,
r_pp: DMatrix<f64>,
l_pp: DMatrix<f64>,
r_diag: Vec<f64>,
l_diag: Vec<f64>,
counts: Counts,
operator_seconds: f64,
assembly_seconds: f64,
}
impl IterativeSystem {
pub fn assemble(
geometry: &Geometry,
ports: &[Port],
discretization: &Discretization,
params: &IterativeParams,
) -> Result<Self, SolveError> {
let start = Instant::now();
let (filaments, per_segment) = discretize_geometry(geometry, discretization)?;
let mesh = MeshMatrix::build(geometry, &per_segment, ports)?;
let resistances: Vec<f64> = filaments.iter().map(filament_resistance).collect();
let operator_start = Instant::now();
let operator = PfftOperator::new(&filaments, ¶ms.pfft)?;
let operator_seconds = operator_start.elapsed().as_secs_f64();
let (internal, port_count) = (mesh.internal_count(), mesh.port_count());
let branches = filaments.len();
let mut r_ep = DMatrix::zeros(internal, port_count);
let mut l_ep = DMatrix::zeros(internal, port_count);
let mut r_pp = DMatrix::zeros(port_count, port_count);
let mut l_pp = DMatrix::zeros(port_count, port_count);
for p in 0..port_count {
let mut current = vec![0.0; branches];
for &(branch, sign) in mesh.row(internal + p) {
current[branch] += sign;
}
let flux = operator.apply(¤t);
let drop: Vec<f64> = current
.iter()
.zip(&resistances)
.map(|(c, r)| c * r)
.collect();
let r_column = gather_all(&mesh, &drop);
let l_column = gather_all(&mesh, &flux);
for i in 0..internal {
r_ep[(i, p)] = r_column[i];
l_ep[(i, p)] = l_column[i];
}
for q in 0..port_count {
r_pp[(q, p)] = r_column[internal + q];
l_pp[(q, p)] = l_column[internal + q];
}
}
let r_pp = (&r_pp + r_pp.transpose()) * 0.5;
let l_pp = (&l_pp + l_pp.transpose()) * 0.5;
let diagonals: Vec<(f64, f64)> = (0..internal)
.into_par_iter()
.map(|i| {
let row = mesh.row(i);
let r: f64 = row.iter().map(|&(branch, _)| resistances[branch]).sum();
loop_inductance(&filaments, row).map(|l| (r, l))
})
.collect::<Result<_, KernelError>>()?;
let (r_diag, l_diag) = diagonals.into_iter().unzip();
let counts = Counts {
nodes: geometry.nodes().len(),
segments: geometry.segment_count(),
filaments: branches,
meshes: mesh.loop_count(),
internal_meshes: internal,
ports: port_count,
};
Ok(Self {
ports: ports.to_vec(),
filaments,
resistances,
mesh,
operator,
gmres: params.gmres,
r_ep,
l_ep,
r_pp,
l_pp,
r_diag,
l_diag,
counts,
operator_seconds,
assembly_seconds: start.elapsed().as_secs_f64(),
})
}
pub fn ports(&self) -> &[Port] {
&self.ports
}
pub fn filaments(&self) -> &[Filament] {
&self.filaments
}
pub fn mesh(&self) -> &MeshMatrix {
&self.mesh
}
pub fn counts(&self) -> Counts {
self.counts
}
pub fn operator_stats(&self) -> PfftStats {
self.operator.stats()
}
pub fn gmres_params(&self) -> GmresParams {
self.gmres
}
pub fn impedance(&self, frequency: f64) -> Result<DMatrix<C64>, SolveError> {
self.solve(frequency).map(|solution| solution.impedance)
}
pub fn solve(&self, frequency: f64) -> Result<IterativeSolution, SolveError> {
check_frequency(0, frequency)?;
let (z, outcomes) = if frequency == 0.0 {
let (z, outcomes) = schur_complement_gmres(
&self.r_ep,
&self.r_pp,
|x| self.apply_dc(x),
&self.r_diag,
&self.gmres,
frequency,
)?;
(z.map(|re| C64::new(re, 0.0)), outcomes)
} else {
let omega = std::f64::consts::TAU * frequency;
let complex =
|r: &DMatrix<f64>, l: &DMatrix<f64>| r.zip_map(l, |r, l| C64::new(r, omega * l));
let diagonal: Vec<C64> = self
.r_diag
.iter()
.zip(&self.l_diag)
.map(|(&r, &l)| C64::new(r, omega * l))
.collect();
schur_complement_gmres(
&complex(&self.r_ep, &self.l_ep),
&complex(&self.r_pp, &self.l_pp),
|x| self.apply_ac(omega, x),
&diagonal,
&self.gmres,
frequency,
)?
};
let z = (&z + z.transpose()).map(|e| e * 0.5);
if z.iter().all(|e| e.re.is_finite() && e.im.is_finite()) {
Ok(IterativeSolution {
impedance: z,
gmres: outcomes,
})
} else {
Err(SolveError::Singular { frequency })
}
}
pub fn sweep(&self, frequencies: &[f64]) -> Result<SweepResult, SolveError> {
let start = Instant::now();
for (index, &frequency) in frequencies.iter().enumerate() {
check_frequency(index, frequency)?;
}
let stats = self.operator.stats();
let fft_points: usize = stats.fft_dims.iter().product();
let bytes_per_solve = (self.gmres.restart + 4) * self.counts.internal_meshes * 16
+ 8 * self.counts.filaments * 8
+ 4 * fft_points * 16;
let concurrent = (SWEEP_MEMORY_BUDGET_BYTES / bytes_per_solve.max(1)).max(1);
let mut impedance = Vec::with_capacity(frequencies.len());
for chunk in frequencies.chunks(concurrent) {
let solved: Vec<_> = chunk.par_iter().map(|&f| self.impedance(f)).collect();
for z in solved {
impedance.push(z?);
}
}
let solve_seconds = start.elapsed().as_secs_f64();
Ok(SweepResult {
schema_version: SweepResult::SCHEMA_VERSION,
ports: self.ports.clone(),
frequencies_hz: frequencies.to_vec(),
impedance_ohm: impedance,
provenance: Provenance {
fasterhenry_version: crate::VERSION.to_owned(),
counts: self.counts,
timing: Some(Timing {
threads: rayon::current_num_threads(),
inductance_s: self.operator_seconds,
assembly_s: self.assembly_seconds,
solve_s: solve_seconds,
total_s: self.assembly_seconds + solve_seconds,
}),
},
})
}
fn scatter<T: ComplexField<RealField = f64> + Copy>(&self, x: &[T]) -> Vec<T> {
let mut branch = vec![T::zero(); self.filaments.len()];
for (i, &xi) in x.iter().enumerate() {
for &(b, sign) in self.mesh.row(i) {
branch[b] += xi.scale(sign);
}
}
branch
}
fn apply_dc(&self, x: &[f64]) -> Vec<f64> {
let mut drop = self.scatter(x);
for (d, r) in drop.iter_mut().zip(&self.resistances) {
*d *= r;
}
gather(&self.mesh, x.len(), &drop)
}
fn apply_ac(&self, omega: f64, x: &[C64]) -> Vec<C64> {
let current = self.scatter(x);
let re: Vec<f64> = current.iter().map(|c| c.re).collect();
let im: Vec<f64> = current.iter().map(|c| c.im).collect();
let (flux_re, flux_im) =
rayon::join(|| self.operator.apply(&re), || self.operator.apply(&im));
let drop: Vec<C64> = (0..current.len())
.map(|b| {
let r = self.resistances[b];
C64::new(
r * re[b] - omega * flux_im[b],
r * im[b] + omega * flux_re[b],
)
})
.collect();
gather(&self.mesh, x.len(), &drop)
}
}
fn schur_complement_gmres<T>(
ep: &DMatrix<T>,
pp: &DMatrix<T>,
apply_ee: impl Fn(&[T]) -> Vec<T>,
diagonal: &[T],
params: &GmresParams,
frequency: f64,
) -> Result<(DMatrix<T>, Vec<GmresOutcome>), SolveError>
where
T: ComplexField<RealField = f64> + Copy,
{
let internal = ep.nrows();
if internal == 0 {
return Ok((pp.clone(), Vec::new()));
}
let mut solved = DMatrix::zeros(internal, ep.ncols());
let mut outcomes = Vec::with_capacity(ep.ncols());
for port in 0..ep.ncols() {
let rhs: Vec<T> = ep.column(port).iter().copied().collect();
let mut x = vec![T::zero(); internal];
let outcome = gmres(
&apply_ee,
|v: &[T]| v.iter().zip(diagonal).map(|(&v, &d)| v / d).collect(),
&rhs,
&mut x,
params,
);
if !outcome.converged {
return Err(SolveError::NotConverged {
frequency,
port,
iterations: outcome.iterations,
relative_residual: outcome.relative_residual,
});
}
solved.column_mut(port).copy_from_slice(&x);
outcomes.push(outcome);
}
Ok((pp - ep.transpose() * solved, outcomes))
}
fn gather<T: ComplexField<RealField = f64> + Copy>(
mesh: &MeshMatrix,
loops: usize,
v: &[T],
) -> Vec<T> {
(0..loops)
.into_par_iter()
.with_min_len(256)
.map(|i| {
mesh.row(i)
.iter()
.fold(T::zero(), |sum, &(b, sign)| sum + v[b].scale(sign))
})
.collect()
}
fn gather_all(mesh: &MeshMatrix, v: &[f64]) -> Vec<f64> {
gather(mesh, mesh.loop_count(), v)
}
fn loop_inductance(filaments: &[Filament], row: &[(usize, f64)]) -> Result<f64, KernelError> {
let own: f64 = row
.iter()
.map(|&(branch, _)| self_inductance(&filaments[branch]))
.sum();
if row.len() > EXACT_DIAGONAL_MAX_LOOP_LENGTH {
return Ok(own);
}
let mut mutual = 0.0;
for (k, &(a, sign_a)) in row.iter().enumerate() {
for &(b, sign_b) in &row[k + 1..] {
let m = mutual_inductance(&filaments[a], &filaments[b]).map_err(|e| e.at(a, b))?;
mutual += sign_a * sign_b * m;
}
}
Ok(own + 2.0 * mutual)
}