use std::time::Instant;
use nalgebra::{ComplexField, DMatrix};
use num_complex::Complex;
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::coupling::{Coupling, CouplingError, TruncationWarning};
use crate::dense::lu_solve;
use crate::filament::{
discretize, discretize_graded, graded_surface_extent, DiscretizeError, Filament,
};
use crate::geometry::Geometry;
use crate::inductance::{
partial_inductance_matrix, partial_inductance_matrix_masked, KernelError, MU0,
};
use crate::mesh::{MeshError, MeshMatrix, Port};
use crate::pfft::PfftError;
use crate::result::{Counts, Provenance, SweepResult, Timing};
pub(crate) const SWEEP_MEMORY_BUDGET_BYTES: usize = 2 << 30;
pub const DENSE_PATH_MAX_FILAMENTS: usize = 10_000;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Solver {
Dense,
Iterative,
}
impl std::fmt::Display for Solver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Dense => "dense",
Self::Iterative => "iterative",
})
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SolverChoice {
#[default]
Auto,
Dense,
Iterative,
}
impl SolverChoice {
pub const fn resolve(self, filaments: usize) -> Solver {
match self {
Self::Dense => Solver::Dense,
Self::Iterative => Solver::Iterative,
Self::Auto if filaments <= DENSE_PATH_MAX_FILAMENTS => Solver::Dense,
Self::Auto => Solver::Iterative,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Subdivision {
pub nw: usize,
pub nh: usize,
}
impl Subdivision {
pub const fn new(nw: usize, nh: usize) -> Self {
Self { nw, nh }
}
pub const SINGLE: Self = Self::new(1, 1);
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Grading {
pub nw: usize,
pub nh: usize,
pub ratio: f64,
}
impl Grading {
pub const fn new(nw: usize, nh: usize, ratio: f64) -> Self {
Self { nw, nh, ratio }
}
fn validate(&self) -> Result<(), SolveError> {
if self.ratio.is_finite() && self.ratio >= 1.0 {
Ok(())
} else {
Err(SolveError::InvalidGrading {
reason: format!(
"the grading ratio must be finite and at least 1, got {}",
self.ratio
),
})
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct SkinDepthGrading {
pub frequency_hz: f64,
pub target_skin_depths: f64,
pub ratio: f64,
pub max_per_axis: usize,
}
impl SkinDepthGrading {
pub const DEFAULT_MAX_PER_AXIS: usize = 32;
pub const fn new(frequency_hz: f64, target_skin_depths: f64, ratio: f64) -> Self {
Self {
frequency_hz,
target_skin_depths,
ratio,
max_per_axis: Self::DEFAULT_MAX_PER_AXIS,
}
}
pub const fn with_max_per_axis(mut self, max_per_axis: usize) -> Self {
self.max_per_axis = max_per_axis;
self
}
fn validate(&self) -> Result<(), SolveError> {
let invalid = |reason: String| Err(SolveError::InvalidGrading { reason });
if !(self.ratio.is_finite() && self.ratio >= 1.0) {
return invalid(format!(
"the grading ratio must be finite and at least 1, got {}",
self.ratio
));
}
if !(self.target_skin_depths.is_finite() && self.target_skin_depths > 0.0) {
return invalid(format!(
"target_skin_depths must be finite and positive, got {}",
self.target_skin_depths
));
}
if !(self.frequency_hz.is_finite() && self.frequency_hz >= 0.0) {
return invalid(format!(
"frequency_hz must be finite and non-negative, got {}",
self.frequency_hz
));
}
if self.max_per_axis == 0 {
return invalid("max_per_axis must be at least 1".to_owned());
}
Ok(())
}
fn count_for(&self, extent: f64, sigma: f64) -> usize {
let target = self.target_skin_depths * skin_depth(self.frequency_hz, sigma);
if target.is_infinite() || extent <= target {
return 1;
}
for count in 2..=self.max_per_axis {
match graded_surface_extent(extent, count, self.ratio) {
Ok(surface) if surface <= target => return count,
Err(_) => return count - 1,
Ok(_) => {}
}
}
self.max_per_axis
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Discretization {
Uniform(Subdivision),
PerSegment(Vec<Subdivision>),
Graded(Grading),
SkinDepth(SkinDepthGrading),
}
impl Discretization {
pub const fn uniform(nw: usize, nh: usize) -> Self {
Self::Uniform(Subdivision::new(nw, nh))
}
pub const fn graded(nw: usize, nh: usize, ratio: f64) -> Self {
Self::Graded(Grading::new(nw, nh, ratio))
}
pub const fn skin_depth(frequency_hz: f64, target_skin_depths: f64, ratio: f64) -> Self {
Self::SkinDepth(SkinDepthGrading::new(
frequency_hz,
target_skin_depths,
ratio,
))
}
pub fn filament_count(&self, geometry: &Geometry) -> Result<usize, SolveError> {
Ok(self
.resolve(geometry)?
.iter()
.map(|grid| grid.nw * grid.nh)
.sum())
}
fn resolve(&self, geometry: &Geometry) -> Result<Vec<SegmentGrid>, SolveError> {
let segment_count = geometry.segment_count();
match self {
Self::Uniform(subdivision) => Ok(vec![SegmentGrid::from(*subdivision); segment_count]),
Self::PerSegment(list) if list.len() == segment_count => {
Ok(list.iter().copied().map(SegmentGrid::from).collect())
}
Self::PerSegment(list) => Err(SolveError::Mesh(MeshError::SegmentCountMismatch {
expected: segment_count,
got: list.len(),
})),
Self::Graded(grading) => {
grading.validate()?;
Ok(vec![SegmentGrid::from(*grading); segment_count])
}
Self::SkinDepth(grading) => {
grading.validate()?;
Ok(geometry
.segments()
.map(|segment| SegmentGrid {
nw: grading.count_for(segment.width, segment.sigma),
nh: grading.count_for(segment.height, segment.sigma),
ratio: grading.ratio,
})
.collect())
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct SegmentGrid {
nw: usize,
nh: usize,
ratio: f64,
}
impl From<Subdivision> for SegmentGrid {
fn from(subdivision: Subdivision) -> Self {
Self {
nw: subdivision.nw,
nh: subdivision.nh,
ratio: 1.0,
}
}
}
impl From<Grading> for SegmentGrid {
fn from(grading: Grading) -> Self {
Self {
nw: grading.nw,
nh: grading.nh,
ratio: grading.ratio,
}
}
}
pub fn skin_depth(frequency_hz: f64, sigma: f64) -> f64 {
1.0 / (std::f64::consts::PI * frequency_hz * MU0 * sigma).sqrt()
}
#[derive(Clone, Debug, PartialEq, Error)]
pub enum SolveError {
#[error("segment {segment}: {source}")]
Discretize {
segment: usize,
source: DiscretizeError,
},
#[error(transparent)]
Kernel(#[from] KernelError),
#[error(transparent)]
Mesh(#[from] MeshError),
#[error("invalid grading: {reason}")]
InvalidGrading {
reason: String,
},
#[error(transparent)]
Coupling(#[from] CouplingError),
#[error("frequency {index} is {value} Hz; frequencies must be finite and non-negative")]
InvalidFrequency {
index: usize,
value: f64,
},
#[error("the mesh system is singular or ill-conditioned at {frequency} Hz")]
Singular {
frequency: f64,
},
#[error(transparent)]
Pfft(#[from] PfftError),
#[error(
"GMRES did not converge at {frequency} Hz for port {port}: relative residual \
{relative_residual:e} after {iterations} iterations"
)]
NotConverged {
frequency: f64,
port: usize,
iterations: usize,
relative_residual: f64,
},
}
#[derive(Clone, Debug)]
pub struct MeshSystem {
ports: Vec<Port>,
filaments: Vec<Filament>,
resistances: Vec<f64>,
mesh: MeshMatrix,
r_ee: DMatrix<f64>,
r_ep: DMatrix<f64>,
r_pp: DMatrix<f64>,
l_ee: DMatrix<f64>,
l_ep: DMatrix<f64>,
l_pp: DMatrix<f64>,
counts: Counts,
truncation_warnings: Vec<TruncationWarning>,
inductance_seconds: f64,
assembly_seconds: f64,
}
impl MeshSystem {
pub fn assemble(
geometry: &Geometry,
ports: &[Port],
discretization: &Discretization,
) -> Result<Self, SolveError> {
Self::assemble_with_coupling(geometry, ports, discretization, &Coupling::all_pairs())
}
pub fn assemble_with_coupling(
geometry: &Geometry,
ports: &[Port],
discretization: &Discretization,
coupling: &Coupling,
) -> Result<Self, SolveError> {
let start = Instant::now();
let truncation_warnings = coupling.truncation_warnings(geometry)?;
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 inductance_start = Instant::now();
let inductance = if coupling.is_all_pairs() {
partial_inductance_matrix(&filaments)?
} else {
partial_inductance_matrix_masked(&filaments, &coupling.pair_mask(&per_segment)?)?
};
let inductance_seconds = inductance_start.elapsed().as_secs_f64();
let branches = filaments.len();
let r_mesh = congruence(&mesh, branches, |branch, sign, row| {
row[branch] += sign * resistances[branch];
});
let l_mesh = congruence(&mesh, branches, |branch, sign, row| {
for (slot, value) in row.iter_mut().zip(inductance.column(branch).iter()) {
*slot += sign * value;
}
});
drop(inductance);
let (internal, port_count) = (mesh.internal_count(), mesh.port_count());
let block = |m: &DMatrix<f64>, r0, c0, nr, nc| m.view((r0, c0), (nr, nc)).into_owned();
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,
r_ee: block(&r_mesh, 0, 0, internal, internal),
r_ep: block(&r_mesh, 0, internal, internal, port_count),
r_pp: block(&r_mesh, internal, internal, port_count, port_count),
l_ee: block(&l_mesh, 0, 0, internal, internal),
l_ep: block(&l_mesh, 0, internal, internal, port_count),
l_pp: block(&l_mesh, internal, internal, port_count, port_count),
mesh,
counts,
truncation_warnings,
inductance_seconds,
assembly_seconds: start.elapsed().as_secs_f64(),
})
}
pub fn ports(&self) -> &[Port] {
&self.ports
}
pub fn filaments(&self) -> &[Filament] {
&self.filaments
}
pub fn filament_resistances(&self) -> &[f64] {
&self.resistances
}
pub fn mesh(&self) -> &MeshMatrix {
&self.mesh
}
pub fn counts(&self) -> Counts {
self.counts
}
pub fn truncation_warnings(&self) -> &[TruncationWarning] {
&self.truncation_warnings
}
pub fn impedance(&self, frequency: f64) -> Result<DMatrix<Complex<f64>>, SolveError> {
check_frequency(0, frequency)?;
let singular = SolveError::Singular { frequency };
let z = if frequency == 0.0 {
schur_complement(self.r_ee.clone(), &self.r_ep, self.r_pp.clone())
.ok_or(singular.clone())?
.map(|re| Complex::new(re, 0.0))
} else {
let omega = std::f64::consts::TAU * frequency;
let complex = |r: &DMatrix<f64>, l: &DMatrix<f64>| {
r.zip_map(l, |r, l| Complex::new(r, omega * l))
};
schur_complement(
complex(&self.r_ee, &self.l_ee),
&complex(&self.r_ep, &self.l_ep),
complex(&self.r_pp, &self.l_pp),
)
.ok_or(singular.clone())?
};
if z.iter()
.all(|entry| entry.re.is_finite() && entry.im.is_finite())
{
Ok(z)
} else {
Err(singular)
}
}
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 internal = self.counts.internal_meshes;
let bytes_per_solve = 2 * internal * (internal + self.counts.ports) * 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.inductance_seconds,
assembly_s: self.assembly_seconds,
solve_s: solve_seconds,
total_s: self.assembly_seconds + solve_seconds,
}),
},
})
}
}
pub fn solve(
geometry: &Geometry,
ports: &[Port],
discretization: &Discretization,
frequencies: &[f64],
) -> Result<SweepResult, SolveError> {
MeshSystem::assemble(geometry, ports, discretization)?.sweep(frequencies)
}
pub fn filament_resistance(filament: &Filament) -> f64 {
filament.length() / (filament.sigma() * filament.area())
}
pub(crate) fn discretize_geometry(
geometry: &Geometry,
discretization: &Discretization,
) -> Result<(Vec<Filament>, Vec<usize>), SolveError> {
let grids = discretization.resolve(geometry)?;
let mut filaments = Vec::new();
let mut per_segment = Vec::with_capacity(grids.len());
for (index, (segment, grid)) in geometry.segments().zip(&grids).enumerate() {
let bundle = match discretization {
Discretization::Uniform(_) | Discretization::PerSegment(_) => {
discretize(&segment, grid.nw, grid.nh)
}
_ => discretize_graded(&segment, grid.nw, grid.nh, grid.ratio),
}
.map_err(|source| SolveError::Discretize {
segment: index,
source,
})?;
per_segment.push(bundle.len());
filaments.extend(bundle);
}
Ok((filaments, per_segment))
}
pub(crate) fn check_frequency(index: usize, value: f64) -> Result<(), SolveError> {
if value.is_finite() && value >= 0.0 {
Ok(())
} else {
Err(SolveError::InvalidFrequency { index, value })
}
}
fn schur_complement<T: ComplexField + Copy + Send + Sync>(
ee: DMatrix<T>,
ep: &DMatrix<T>,
pp: DMatrix<T>,
) -> Option<DMatrix<T>> {
if ee.is_empty() {
return Some(pp);
}
let solved = lu_solve(ee, ep)?;
Some(pp - ep.transpose() * solved)
}
fn congruence(
mesh: &MeshMatrix,
branches: usize,
accumulate: impl Fn(usize, f64, &mut [f64]) + Sync,
) -> DMatrix<f64> {
let loops = mesh.loop_count();
if loops == 0 || branches == 0 {
return DMatrix::zeros(loops, loops);
}
let mut upper = vec![0.0; loops * loops];
upper.par_chunks_mut(loops).enumerate().for_each_init(
|| vec![0.0; branches],
|product, (i, out)| {
product.fill(0.0);
for &(branch, sign) in mesh.row(i) {
accumulate(branch, sign, product);
}
for (j, slot) in out.iter_mut().enumerate().skip(i) {
*slot = mesh
.row(j)
.iter()
.map(|&(branch, sign)| sign * product[branch])
.sum();
}
},
);
DMatrix::from_fn(loops, loops, |i, j| upper[i.min(j) * loops + i.max(j)])
}
#[cfg(test)]
mod solver_choice_tests {
use super::*;
#[test]
fn auto_switches_paths_at_the_threshold() {
let auto = SolverChoice::default();
assert_eq!(auto, SolverChoice::Auto);
assert_eq!(auto.resolve(0), Solver::Dense);
assert_eq!(auto.resolve(DENSE_PATH_MAX_FILAMENTS), Solver::Dense);
assert_eq!(
auto.resolve(DENSE_PATH_MAX_FILAMENTS + 1),
Solver::Iterative
);
}
#[test]
fn an_explicit_choice_ignores_the_threshold() {
for filaments in [1, DENSE_PATH_MAX_FILAMENTS, 10 * DENSE_PATH_MAX_FILAMENTS] {
assert_eq!(SolverChoice::Dense.resolve(filaments), Solver::Dense);
assert_eq!(
SolverChoice::Iterative.resolve(filaments),
Solver::Iterative
);
}
}
#[test]
fn paths_print_as_their_command_line_names() {
assert_eq!(Solver::Dense.to_string(), "dense");
assert_eq!(Solver::Iterative.to_string(), "iterative");
}
}
#[cfg(test)]
mod bridge_tests {
use super::*;
use crate::geometry::{Geometry, Node, NodeId, SegmentDef};
use crate::mesh::Port;
#[test]
fn bridge_topology_dc_parallel() {
let sigma = 5.8e7;
let (w, t) = (0.2e-3, 35e-6);
let mut g = Geometry::new();
let n1 = NodeId(g.add_node(Node::new(2.5e-3, 0.0, -t / 2.0)).unwrap().0);
let n2 = NodeId(g.add_node(Node::new(7.5e-3, 0.0, -t / 2.0)).unwrap().0);
let a = NodeId(g.add_node(Node::new(2.5e-3, 0.0, 0.5e-3)).unwrap().0);
let b = NodeId(g.add_node(Node::new(7.5e-3, 0.0, 0.5e-3)).unwrap().0);
g.add_segment(SegmentDef::new(n1, n2, 2.0e-3, t, sigma))
.unwrap(); g.add_segment(SegmentDef::new(a, b, w, t, sigma)).unwrap(); g.add_segment(SegmentDef::new(a, n1, w, t, sigma)).unwrap(); g.add_segment(SegmentDef::new(b, n2, w, t, sigma)).unwrap(); let ports = vec![Port::new(a, b)];
let d = Discretization::PerSegment(vec![Subdivision::SINGLE; 4]);
let system = MeshSystem::assemble(&g, &ports, &d).unwrap();
println!(
"r_ee {}\nr_ep {}\nr_pp {}",
system.r_ee, system.r_ep, system.r_pp
);
let r_trace = 5.0e-3 / (sigma * w * t);
let r_path = 2.0 * 0.5175e-3 / (sigma * w * t) + 5.0e-3 / (sigma * 2.0e-3 * t);
let parallel = r_trace * r_path / (r_trace + r_path);
let z = system.impedance(0.0).unwrap();
println!(
"Z = {:.9e}, parallel {parallel:.9e}, trace {r_trace:.9e}",
z[(0, 0)].re
);
assert!(
(z[(0, 0)].re - parallel).abs() < 1e-6 * parallel,
"bridge DC {} vs parallel {}",
z[(0, 0)].re,
parallel
);
}
}