use faer::Mat;
use num_complex::Complex64 as c64;
use super::fit::{
self, Delay, FitError, Options, Pole, Rational, Symmetry, classify, eliminate, expanded,
least_squares, partial_fraction_slopes, partial_fractions, polynomial_terms, real_rows,
residues, sequential, sigma_realization, sigma_zeros, solve_stacked, stable_poles,
starting_poles,
};
use super::model::{band_of, check_band, laplace, responses_of};
use crate::circuit::{Component, Fidelity, Parameter, Port, Provenance, SMatrix, Spectrum, ports};
use crate::units::Wavelength;
use crate::{Error, Result};
const SOLVES: usize = 10;
#[derive(Clone, Debug, PartialEq)]
pub struct Sample {
pub values: Vec<f64>,
pub spectrum: Spectrum,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Interpolation {
Polynomial {
degree: usize,
},
PiecewiseLinear,
}
#[derive(Clone, Debug, PartialEq)]
enum Weights {
Monomials(Vec<Vec<usize>>),
Grid(Vec<Vec<f64>>),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Crossing {
pub value: f64,
pub omega: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ParametricModel {
kind: String,
ports: Vec<Port>,
parameters: Vec<Parameter>,
band: (Wavelength, Wavelength),
centre: Vec<f64>,
half: Vec<f64>,
weights: Weights,
symmetry: Symmetry,
constant: bool,
basis: Vec<Pole>,
numerator: Vec<Vec<f64>>,
denominator: Vec<f64>,
delay: f64,
error: FitError,
basis_error: FitError,
source: String,
}
fn monomials(vars: usize, degree: usize) -> Vec<Vec<usize>> {
fn fill(i: usize, left: usize, current: &mut Vec<usize>, out: &mut Vec<Vec<usize>>) {
if i + 1 == current.len() {
current[i] = left;
out.push(current.clone());
return;
}
for e in (0..=left).rev() {
current[i] = e;
fill(i + 1, left - e, current, out);
}
}
let mut out = vec![vec![0; vars]];
if vars > 0 {
for d in 1..=degree {
fill(0, d, &mut vec![0; vars], &mut out);
}
}
out
}
fn evaluate_monomials(exponents: &[Vec<usize>], x: &[f64]) -> Vec<f64> {
exponents
.iter()
.map(|e| e.iter().zip(x).map(|(&k, &v)| v.powi(k as i32)).product())
.collect()
}
fn grid_weights(axes: &[Vec<f64>], values: &[f64]) -> Vec<f64> {
let count: usize = axes.iter().map(Vec::len).product();
let mut w = vec![0.0; count];
let segments: Vec<(usize, f64)> = axes
.iter()
.zip(values)
.map(|(axis, &v)| {
let k = axis.partition_point(|&a| a <= v).clamp(1, axis.len() - 1);
let t = ((v - axis[k - 1]) / (axis[k] - axis[k - 1])).clamp(0.0, 1.0);
(k - 1, t)
})
.collect();
for corner in 0..(1usize << axes.len()) {
let (mut index, mut stride, mut weight) = (0, 1, 1.0);
for (d, (axis, &(k, t))) in axes.iter().zip(&segments).enumerate() {
let upper = corner >> d & 1 == 1;
index += (k + usize::from(upper)) * stride;
weight *= if upper { t } else { 1.0 - t };
stride *= axis.len();
}
w[index] += weight;
}
w
}
fn error_over(
samples: &[(Vec<Vec<c64>>, Vec<c64>)],
value: impl Fn(usize, usize, usize) -> c64,
) -> FitError {
let (mut sum_e, mut sum_f, mut max_e, mut max_f, mut count) =
(0.0, 0.0, 0.0f64, 0.0f64, 0usize);
for (i, (responses, _)) in samples.iter().enumerate() {
for (e, f) in responses.iter().enumerate() {
for (k, &v) in f.iter().enumerate() {
let d = (value(i, k, e) - v).norm();
sum_e += d * d;
sum_f += v.norm_sqr();
max_e = max_e.max(d);
max_f = max_f.max(v.norm());
count += 1;
}
}
}
let n = count.max(1) as f64;
let (rms, rms_f) = ((sum_e / n).sqrt(), (sum_f / n).sqrt());
FitError {
rms,
max: max_e,
rms_relative: if rms_f > 0.0 { rms / rms_f } else { rms },
max_relative: if max_f > 0.0 { max_e / max_f } else { max_e },
}
}
fn unknowns_of(basis: &[Pole], values: &[c64]) -> Vec<f64> {
let mut out = Vec::new();
let mut k = 0;
for &p in basis {
match p {
Pole::Real(_) => {
out.push(values[k].re);
k += 1;
}
Pole::Pair(_) => {
out.push(values[k].re);
out.push(values[k].im);
k += 2;
}
Pole::Complex(_) => {
out.push(values[k].re);
out.push(values[k].im);
k += 1;
}
}
}
out
}
fn rewrite(
local: &Rational,
basis: &[Pole],
symmetry: Symmetry,
constant: bool,
) -> Result<(Vec<f64>, Vec<Vec<f64>>)> {
use faer::linalg::solvers::Solve;
let a = expanded(basis);
let p = &local.poles;
let n = a.len();
if p.len() != n {
return Err(Error::invalid(
"parametric model",
format!("a sample's fit has {} poles for {n} basis poles", p.len()),
));
}
let m = Mat::<c64>::from_fn(n, n, |l, k| 1.0 / (p[l] - a[k]));
let r = m
.partial_piv_lu()
.solve(Mat::<c64>::from_fn(n, 1, |_, _| c64::new(-1.0, 0.0)));
let r: Vec<c64> = (0..n).map(|k| r[(k, 0)]).collect();
if r.iter().any(|v| !(v.re.is_finite() && v.im.is_finite())) {
return Err(Error::invalid(
"parametric model",
"a sample's poles can't be rewritten on the basis poles (a pole on a basis pole)",
));
}
let den = unknowns_of(basis, &r);
let num = (0..local.responses())
.map(|e| {
let big_r: Vec<c64> = (0..n).map(|k| r[k] * local.evaluate(e, a[k])).collect();
let mut x = unknowns_of(basis, &big_r);
if constant {
let q0 = local.constant[e];
x.push(q0.re);
if symmetry == Symmetry::Complex {
x.push(q0.im);
}
}
x
})
.collect();
Ok((den, num))
}
impl ParametricModel {
pub fn fit(
parameters: Vec<Parameter>,
samples: &[Sample],
options: &Options,
interpolation: Interpolation,
) -> Result<ParametricModel> {
sequential(|| Self::fit_on_one_thread(parameters, samples, options, interpolation))
}
fn fit_on_one_thread(
parameters: Vec<Parameter>,
samples: &[Sample],
options: &Options,
interpolation: Interpolation,
) -> Result<ParametricModel> {
let bad = |reason: String| Err(Error::invalid("parametric model", reason));
let Some(first) = samples.first() else {
return bad("needs samples".into());
};
if options.proportional {
return bad("has no proportional term: an S-matrix stays bounded".into());
}
let delay = match options.delay {
Delay::None => 0.0,
Delay::Fixed(t) => t,
Delay::Estimate => {
return bad("takes a fixed delay or none, not an estimated one".into());
}
};
let vars = parameters.len();
for s in samples {
if s.values.len() != vars {
return bad(format!(
"a sample has {} values for {vars} parameters",
s.values.len()
));
}
if let Some((p, v)) = parameters
.iter()
.zip(&s.values)
.find(|(p, v)| !p.allows(**v))
{
return bad(format!(
"{} = {v} is outside its range, {} to {}",
p.name, p.min, p.max
));
}
if s.spectrum.ports() != first.spectrum.ports()
|| s.spectrum.wavelengths() != first.spectrum.wavelengths()
{
return bad(
"every sample's spectrum must have the same ports and wavelengths".into(),
);
}
}
let wavelengths = first.spectrum.wavelengths();
if wavelengths.len() < 2 || first.spectrum.ports().is_empty() {
return bad("needs spectra of at least two wavelengths and a port".into());
}
let (mut centre, mut half) = (Vec::new(), Vec::new());
for (i, p) in parameters.iter().enumerate() {
let lo = samples
.iter()
.map(|s| s.values[i])
.fold(f64::INFINITY, f64::min);
let hi = samples
.iter()
.map(|s| s.values[i])
.fold(f64::NEG_INFINITY, f64::max);
if hi <= lo {
return bad(format!("{} doesn't vary over the samples", p.name));
}
centre.push(0.5 * (lo + hi));
half.push(0.5 * (hi - lo));
}
let weights = match interpolation {
Interpolation::Polynomial { degree } => {
let exponents = monomials(vars, degree);
if samples.len() < exponents.len() {
return bad(format!(
"{} samples can't determine a polynomial of degree {degree} in {vars} parameters ({} coefficients)",
samples.len(),
exponents.len()
));
}
Weights::Monomials(exponents)
}
Interpolation::PiecewiseLinear => {
let axes: Vec<Vec<f64>> = (0..vars)
.map(|i| {
let mut v: Vec<f64> = samples.iter().map(|s| s.values[i]).collect();
v.sort_by(f64::total_cmp);
v.dedup();
v
})
.collect();
let nodes: usize = axes.iter().map(Vec::len).product();
if nodes != samples.len() {
return bad(format!(
"piecewise-linear interpolation needs the samples on a full grid: {} samples, but the parameters' values make {nodes} combinations",
samples.len()
));
}
Weights::Grid(axes)
}
};
let s: Vec<c64> = wavelengths.iter().map(|&w| laplace(w)).collect();
let names: Vec<&str> = first.spectrum.ports().iter().map(String::as_str).collect();
let mut model = ParametricModel {
kind: "compact model".into(),
ports: ports(&names),
parameters,
band: band_of(wavelengths),
centre,
half,
weights,
symmetry: options.symmetry,
constant: options.constant,
basis: Vec::new(),
numerator: Vec::new(),
denominator: Vec::new(),
delay,
error: FitError {
rms: 0.0,
max: 0.0,
rms_relative: 0.0,
max_relative: 0.0,
},
basis_error: FitError {
rms: 0.0,
max: 0.0,
rms_relative: 0.0,
max_relative: 0.0,
},
source: format!("spectra at {} parameter samples", samples.len()),
};
let data: Vec<(Vec<Vec<c64>>, Vec<c64>)> = samples
.iter()
.map(|smp| {
let responses = responses_of(&smp.spectrum)
.into_iter()
.map(|r| {
r.iter()
.zip(&s)
.map(|(&v, &z)| v * (z * delay).exp())
.collect()
})
.collect();
Ok((responses, model.weights_at(&smp.values)?))
})
.collect::<Result<_>>()?;
let local = Options {
delay: Delay::None,
..options.clone()
};
match &model.weights {
Weights::Monomials(_) => {
let all: Vec<Vec<c64>> = data.iter().flat_map(|(r, _)| r.iter().cloned()).collect();
let shared = fit::vector_fit(&s, &all, &local)?;
model.basis = classify(&shared.poles, options.symmetry)?;
model.basis_error = shared.error;
model.solve(&s, &data)?;
}
Weights::Grid(axes) => {
model.basis = classify(
&starting_poles(&s, options.poles, options.symmetry),
options.symmetry,
)?;
let nodes: usize = axes.iter().map(Vec::len).product();
let n_den: usize = model.basis.iter().map(|p| p.unknowns()).sum();
let n_terms = usize::from(options.constant)
* if options.symmetry == Symmetry::Complex {
2
} else {
1
};
let n_resp = data[0].0.len();
model.denominator = vec![0.0; n_den * nodes];
model.numerator = vec![vec![0.0; (n_den + n_terms) * nodes]; n_resp];
let mut worst = FitError {
rms: 0.0,
max: 0.0,
rms_relative: 0.0,
max_relative: 0.0,
};
for (responses, w) in &data {
let node = w
.iter()
.position(|x| (x.re - 1.0).abs() < 1e-12)
.ok_or_else(|| {
Error::invalid("parametric model", "a sample isn't a node of its grid")
})?;
let fit = fit::vector_fit(&s, responses, &local)?;
let (den, num) =
rewrite(&fit, &model.basis, options.symmetry, options.constant)?;
for (j, v) in den.into_iter().enumerate() {
model.denominator[j * nodes + node] = v;
}
for (e, x) in num.into_iter().enumerate() {
for (j, v) in x.into_iter().enumerate() {
model.numerator[e][j * nodes + node] = v;
}
}
worst = FitError {
rms: worst.rms.max(fit.error.rms),
max: worst.max.max(fit.error.max),
rms_relative: worst.rms_relative.max(fit.error.rms_relative),
max_relative: worst.max_relative.max(fit.error.max_relative),
};
}
model.basis_error = worst;
}
}
let originals: Vec<(Vec<Vec<c64>>, Vec<c64>)> = samples
.iter()
.zip(&data)
.map(|(smp, (_, m))| (responses_of(&smp.spectrum), m.clone()))
.collect();
model.error = error_over(&originals, |i, k, e| {
model.evaluate(e, s[k], &originals[i].1) * (-s[k] * delay).exp()
});
Ok(model)
}
fn columns(&self, s: c64, m: &[c64]) -> (Vec<c64>, Vec<c64>) {
let mut phi = Vec::new();
partial_fractions(&self.basis, s, &mut phi);
let mut terms = Vec::new();
polynomial_terms(&self.options(), s, &mut terms);
let d: Vec<c64> = phi
.iter()
.flat_map(|&p| m.iter().map(move |&x| p * x))
.collect();
let mut n = d.clone();
n.extend(terms.iter().flat_map(|&t| m.iter().map(move |&x| t * x)));
(n, d)
}
fn options(&self) -> Options {
Options {
symmetry: self.symmetry,
constant: self.constant,
proportional: false,
..Options::new(1)
}
}
fn evaluate(&self, e: usize, s: c64, m: &[c64]) -> c64 {
let (n, d) = self.columns(s, m);
let num: c64 = n.iter().zip(&self.numerator[e]).map(|(c, x)| c * x).sum();
let den: c64 = d
.iter()
.zip(&self.denominator)
.map(|(c, x)| c * x)
.sum::<c64>()
+ 1.0;
num / den
}
fn solve(&mut self, s: &[c64], data: &[(Vec<Vec<c64>>, Vec<c64>)]) -> Result<()> {
let n_resp = data[0].0.len();
let k = s.len();
let mut weights = vec![vec![1.0; k]; data.len()];
let mut best: Option<(f64, Vec<Vec<f64>>, Vec<f64>)> = None;
let mut since_best = 0;
for _ in 0..SOLVES {
let mut stacked = Vec::new();
let (mut own, mut n_shared) = (0, 0);
for e in 0..n_resp {
let mut rows = Vec::with_capacity(data.len() * k);
for (i, (responses, m)) in data.iter().enumerate() {
for (j, &z) in s.iter().enumerate() {
let w = weights[i][j];
let f = responses[e][j];
let (n, d) = self.columns(z, m);
own = n.len();
n_shared = d.len();
let mut row: Vec<c64> = n.iter().map(|&v| v * w).collect();
row.extend(d.iter().map(|&v| -f * v * w));
row.push(f * w);
rows.push(row);
}
}
eliminate(real_rows(&rows), own, n_shared, &mut stacked);
}
let c_tilde = solve_stacked(&stacked, n_shared);
let mut numerator = Vec::with_capacity(n_resp);
for e in 0..n_resp {
let mut rows = Vec::with_capacity(data.len() * k);
let mut rhs = Vec::with_capacity(data.len() * k);
for (i, (responses, m)) in data.iter().enumerate() {
for (j, &z) in s.iter().enumerate() {
let w = weights[i][j];
let (n, d) = self.columns(z, m);
let den: c64 =
d.iter().zip(&c_tilde).map(|(c, x)| c * x).sum::<c64>() + 1.0;
rows.push(n.iter().map(|&v| v * w).collect::<Vec<c64>>());
rhs.push(responses[e][j] * den * w);
}
}
let a = real_rows(&rows);
let r = rhs.len();
let b = Mat::from_fn(
2 * r,
1,
|i, _| if i < r { rhs[i].re } else { rhs[i - r].im },
);
numerator.push(least_squares(a, &b));
}
self.numerator = numerator;
self.denominator = c_tilde;
let err = error_over(data, |i, j, e| self.evaluate(e, s[j], &data[i].1)).max;
if best.as_ref().is_none_or(|b| err < b.0) {
best = Some((err, self.numerator.clone(), self.denominator.clone()));
since_best = 0;
} else {
since_best += 1;
if since_best == 3 {
break;
}
}
for (i, (_, m)) in data.iter().enumerate() {
for (j, &z) in s.iter().enumerate() {
let (_, d) = self.columns(z, m);
let den: c64 = d
.iter()
.zip(&self.denominator)
.map(|(c, x)| c * x)
.sum::<c64>()
+ 1.0;
weights[i][j] = 1.0 / den.norm().max(f64::MIN_POSITIVE);
}
}
}
let (_, numerator, denominator) = best.expect("at least one solve");
self.numerator = numerator;
self.denominator = denominator;
Ok(())
}
#[must_use]
pub fn with_kind(mut self, kind: impl Into<String>) -> ParametricModel {
self.kind = kind.into();
self
}
#[must_use]
pub fn with_source(mut self, source: impl Into<String>) -> ParametricModel {
self.source = source.into();
self
}
pub fn error(&self) -> FitError {
self.error
}
pub fn basis_error(&self) -> FitError {
self.basis_error
}
pub fn band(&self) -> (Wavelength, Wavelength) {
self.band
}
fn weights_at(&self, values: &[f64]) -> Result<Vec<c64>> {
if values.len() != self.parameters.len() {
return Err(Error::invalid(
"parametric model",
format!(
"takes {} parameter values, not {}",
self.parameters.len(),
values.len()
),
));
}
let mut x = Vec::with_capacity(values.len());
for ((v, (c, h)), p) in values
.iter()
.zip(self.centre.iter().zip(&self.half))
.zip(&self.parameters)
{
let u = (v - c) / h;
if u.is_nan() || u.abs() > 1.0 + 1e-12 {
return Err(Error::invalid(
"parametric model",
format!(
"{} was sampled from {} to {}, not at {v}",
p.name,
c - h,
c + h
),
));
}
x.push(u);
}
let w = match &self.weights {
Weights::Monomials(exponents) => evaluate_monomials(exponents, &x),
Weights::Grid(axes) => grid_weights(axes, values),
};
Ok(w.into_iter().map(|v| c64::new(v, 0.0)).collect())
}
fn sigma_at(&self, m: &[c64]) -> Vec<f64> {
let l = m.len();
self.denominator
.chunks(l)
.map(|c| c.iter().zip(m).map(|(a, b)| a * b.re).sum())
.collect()
}
pub fn rational(&self, values: &[f64]) -> Result<Rational> {
let m = self.weights_at(values)?;
let c = self.sigma_at(&m);
let poles = sigma_zeros(&self.basis, &c)?;
let n_resp = self.numerator.len();
let mut residues = vec![Vec::with_capacity(poles.len()); n_resp];
for &z in &poles {
let (n, _) = self.columns(z, &m);
let mut slopes = Vec::new();
partial_fraction_slopes(&self.basis, z, &mut slopes);
let d_prime: c64 = slopes.iter().zip(&c).map(|(a, b)| a * b).sum();
for (e, res) in residues.iter_mut().enumerate() {
let num: c64 = n.iter().zip(&self.numerator[e]).map(|(c, x)| c * x).sum();
res.push(num / d_prime);
}
}
let n_pf: usize = self.basis.iter().map(|p| p.unknowns()).sum::<usize>() * m.len();
let constant = (0..n_resp)
.map(|e| {
let x = &self.numerator[e][n_pf..];
match (self.constant, self.symmetry) {
(false, _) => c64::new(0.0, 0.0),
(true, Symmetry::Real) => {
c64::new(x.iter().zip(&m).map(|(a, b)| a * b.re).sum(), 0.0)
}
(true, Symmetry::Complex) => {
let (re, im) = x.split_at(m.len());
c64::new(
re.iter().zip(&m).map(|(a, b)| a * b.re).sum(),
im.iter().zip(&m).map(|(a, b)| a * b.re).sum(),
)
}
}
})
.collect();
Ok(Rational {
poles,
residues,
constant,
proportional: vec![c64::new(0.0, 0.0); n_resp],
delays: vec![self.delay; n_resp],
error: self.error,
iterations: 0,
})
}
pub fn stable_rational(&self, values: &[f64], points: usize) -> Result<Rational> {
let m = self.weights_at(values)?;
let poles = stable_poles(sigma_zeros(&self.basis, &self.sigma_at(&m))?, self.symmetry);
if points < 2 {
return Err(Error::invalid(
"parametric model",
"a refit needs at least two points",
));
}
let (f0, f1) = (1.0 / self.band.1.to_um(), 1.0 / self.band.0.to_um());
let s: Vec<c64> = (0..points)
.map(|k| {
c64::new(
0.0,
-std::f64::consts::TAU * (f0 + (f1 - f0) * k as f64 / (points - 1) as f64),
)
})
.collect();
let n_resp = self.numerator.len();
let data: Vec<Vec<c64>> = (0..n_resp)
.map(|e| s.iter().map(|&z| self.evaluate(e, z, &m)).collect())
.collect();
let original: Vec<Vec<c64>> = data
.iter()
.map(|f| {
f.iter()
.zip(&s)
.map(|(&v, &z)| v * (-z * self.delay).exp())
.collect()
})
.collect();
let unknowns: usize = poles.iter().map(|p| p.unknowns()).sum::<usize>() + 2;
if 2 * points < unknowns + 1 {
return Err(Error::invalid(
"parametric model",
format!("{points} points can't determine {unknowns} unknowns per response"),
));
}
residues(
&s,
&data,
&poles,
&self.options(),
&vec![self.delay; n_resp],
&original,
0,
)
}
pub fn error_against(&self, samples: &[Sample]) -> Result<FitError> {
let mut data = Vec::with_capacity(samples.len());
for smp in samples {
if smp.spectrum.ports().len() != self.ports.len() {
return Err(Error::invalid(
"parametric model",
"a sample's ports don't match the model's",
));
}
for &w in smp.spectrum.wavelengths() {
check_band("parametric model", self.band, w)?;
}
data.push((responses_of(&smp.spectrum), self.weights_at(&smp.values)?));
}
let s: Vec<Vec<c64>> = samples
.iter()
.map(|smp| {
smp.spectrum
.wavelengths()
.iter()
.map(|&w| laplace(w))
.collect()
})
.collect();
Ok(error_over(&data, |i, k, e| {
let z = s[i][k];
self.evaluate(e, z, &data[i].1) * (-z * self.delay).exp()
}))
}
pub fn stability(&self, points: usize) -> Result<f64> {
if points < 2 {
return Err(Error::invalid(
"stability check",
"needs at least two points",
));
}
let vars = self.parameters.len();
let per = points.min((1e4f64.powf(1.0 / vars.max(1) as f64)).floor().max(2.0) as usize);
let total = per.pow(vars as u32);
let mut worst = f64::NEG_INFINITY;
for k in 0..total {
let mut idx = k;
let values: Vec<f64> = (0..vars)
.map(|d| {
let i = idx % per;
idx /= per;
let u = -1.0 + 2.0 * i as f64 / (per - 1) as f64;
(self.centre[d] + self.half[d] * u)
.clamp(self.centre[d] - self.half[d], self.centre[d] + self.half[d])
})
.collect();
let m = self.weights_at(&values)?;
for a in sigma_zeros(&self.basis, &self.sigma_at(&m))? {
worst = worst.max(a.re / a.norm());
}
}
Ok(worst)
}
pub fn uniform_stability(&self) -> Result<Vec<Crossing>> {
let Weights::Grid(axes) = &self.weights else {
return Err(Error::invalid(
"uniform stability",
"decides piecewise-linear models only: a polynomial model's poles are sampled by stability()",
));
};
if axes.len() != 1 {
return Err(Error::invalid(
"uniform stability",
"decides models of one parameter only",
));
}
let axis = &axes[0];
let failed = |e: String| Error::invalid("uniform stability", e);
let node = |k: usize| -> Vec<f64> {
let m: Vec<c64> = (0..axis.len())
.map(|l| c64::new(f64::from(u8::from(l == k)), 0.0))
.collect();
self.sigma_at(&m)
};
let mut crossings = Vec::new();
for (k, &value) in axis.iter().enumerate() {
for z in sigma_zeros(&self.basis, &node(k))? {
if z.re >= 0.0 {
crossings.push(Crossing {
value,
omega: -z.im,
});
}
}
}
for k in 0..axis.len() - 1 {
let (a, b, ck) = sigma_realization(&self.basis, &node(k));
let (_, _, ck1) = sigma_realization(&self.basis, &node(k + 1));
let n = b.len();
let h = Mat::<c64>::from_fn(n, n, |i, j| a[(i, j)] - b[i] * ck[j]);
let delta: Vec<c64> = ck1.iter().zip(&ck).map(|(x, y)| x - y).collect();
let size = 2 * n + 1;
let pencil_a = Mat::<c64>::from_fn(size, size, |i, j| match (i < 2 * n, j < 2 * n) {
(true, true) => {
if i < n && j < n {
h[(i, j)]
} else if i >= n && j >= n {
-h[(i - n, j - n)].conj()
} else {
c64::new(0.0, 0.0)
}
}
(true, false) => {
if i < n {
b[i]
} else {
b[i - n].conj()
}
}
(false, true) => {
if j < n {
delta[j]
} else {
delta[j - n].conj()
}
}
(false, false) => c64::new(0.0, 0.0),
});
let pencil_b = Mat::<c64>::from_fn(size, size, |i, j| {
c64::new(f64::from(u8::from(i == j && i < 2 * n)), 0.0)
});
let eig = pencil_a
.generalized_eigen(&pencil_b)
.map_err(|e| failed(format!("the axis crossings: {e:?}")))?;
let (alpha, beta) = (eig.S_a(), eig.S_b());
let scale = (0..n).map(|i| h[(i, i)].norm()).fold(1.0, f64::max);
for i in 0..size {
if beta[i].norm() <= 1e-12 * alpha[i].norm() {
continue; }
let z = alpha[i] / beta[i];
if z.re.abs() > 1e-4 * scale {
continue;
}
let g =
{
use faer::linalg::solvers::Solve;
let x = Mat::<c64>::from_fn(n, n, |r, c| {
if r == c { z - h[(r, c)] } else { -h[(r, c)] }
})
.partial_piv_lu()
.solve(Mat::<c64>::from_fn(n, 1, |r, _| b[r]));
(0..n).map(|r| delta[r] * x[(r, 0)]).sum::<c64>()
};
if !(g.re < 0.0 && g.re.is_finite()) {
continue;
}
let t0 = (-1.0 / g.re).clamp(0.0, 1.0);
let target = c64::new(0.0, z.im);
let nearest = |t: f64| -> Result<c64> {
let m = Mat::<c64>::from_fn(n, n, |r, c| h[(r, c)] - t * b[r] * delta[c]);
let values = m
.eigenvalues()
.map_err(|e| failed(format!("the poles along a segment: {e:?}")))?;
values
.into_iter()
.min_by(|x, y| (x - target).norm().total_cmp(&(y - target).norm()))
.ok_or_else(|| failed("a segment without poles".into()))
};
let f0 = nearest(t0)?.re;
for side in [-1.0, 1.0] {
let mut bracket = None;
for width in [1e-8, 1e-6, 1e-4, 1e-2, 1e-1] {
let t1 = (t0 + side * width).clamp(0.0, 1.0);
if t1 == t0 {
break;
}
if nearest(t1)?.re * f0 <= 0.0 {
bracket = Some((t0.min(t1), t0.max(t1)));
break;
}
}
let Some((mut lo, mut hi)) = bracket else {
continue;
};
let f_lo = nearest(lo)?.re;
for _ in 0..60 {
let mid = 0.5 * (lo + hi);
if nearest(mid)?.re * f_lo > 0.0 {
lo = mid;
} else {
hi = mid;
}
}
let t = 0.5 * (lo + hi);
let pole = nearest(t)?;
if pole.re.abs() > 1e-6 * pole.norm() {
continue;
}
let crossing = Crossing {
value: axis[k] + t * (axis[k + 1] - axis[k]),
omega: -pole.im,
};
let span = axis[k + 1] - axis[k];
if !crossings.iter().any(|c: &Crossing| {
(c.value - crossing.value).abs() <= 1e-9 * span
&& (c.omega - crossing.omega).abs()
<= 1e-9 * crossing.omega.abs().max(1.0)
}) {
crossings.push(crossing);
}
}
}
}
crossings.sort_by(|x, y| x.value.total_cmp(&y.value));
Ok(crossings)
}
}
impl Component for ParametricModel {
fn kind(&self) -> &str {
&self.kind
}
fn ports(&self) -> &[Port] {
&self.ports
}
fn parameters(&self) -> &[Parameter] {
&self.parameters
}
fn s_matrix(&self, wavelength: Wavelength, values: &[f64]) -> Result<SMatrix> {
check_band("parametric model", self.band, wavelength)?;
let m = self.weights_at(values)?;
let s = laplace(wavelength);
let delay = (-s * self.delay).exp();
let n = self.ports.len();
Ok(SMatrix::from_fn(n, |q, p| {
self.evaluate(q * n + p, s, &m) * delay
}))
}
fn provenance(&self) -> Provenance {
let dependence = match &self.weights {
Weights::Monomials(exponents) => format!(
"polynomial of degree {} in the parameters (Sanathanan and Koerner's iteration, IEEE Trans. Autom. Control 8, 56 (1963), doi:10.1109/TAC.1963.1105517)",
exponents.iter().map(|e| e.iter().sum::<usize>()).max().unwrap_or(0)
),
Weights::Grid(_) => "piecewise linear in the parameters (P. Triverio et al., IEEE Trans. Adv. Packag. 32, 205 (2009), doi:10.1109/TADVP.2008.2007913)".into(),
};
Provenance {
fidelity: Fidelity::Compact,
source: format!(
"a rational model on {} basis poles (vector fitting, B. Gustavsen, A. Semlyen, IEEE Trans. Power Deliv. 14, 1052 (1999), doi:10.1109/61.772353), numerator and denominator {dependence}, fitted to {}",
expanded(&self.basis).len(),
self.source
),
error: Some(self.error.max),
validity: Some((self.band.0.to_um(), self.band.1.to_um())),
}
}
}
#[cfg(test)]
mod tests;