use crate::figure::{Figure, PaperSize};
use crate::grid::{multi_panel, PanelGridOpts};
use crate::scale::ScaleKind;
use crate::series::Origin;
use crate::stats::normal_quantile;
use crate::strokes::Stroke;
use crate::theme::Theme;
#[derive(Debug, Clone)]
pub struct BodeOpts {
pub cell_width: f64,
pub cell_height: f64,
pub title: Option<String>,
pub xlabel: String,
pub mag_label: String,
pub phase_label: String,
pub theme: Theme,
}
impl Default for BodeOpts {
fn default() -> Self {
Self {
cell_width: 900.0,
cell_height: 320.0,
title: None,
xlabel: "ω".to_string(),
mag_label: "|H| [dB]".to_string(),
phase_label: "∠H [°]".to_string(),
theme: Theme::report_1972(),
}
}
}
impl BodeOpts {
pub fn title(mut self, s: impl Into<String>) -> Self {
self.title = Some(s.into());
self
}
pub fn xlabel(mut self, s: impl Into<String>) -> Self {
self.xlabel = s.into();
self
}
pub fn cell_size(mut self, w: f64, h: f64) -> Self {
self.cell_width = w;
self.cell_height = h;
self
}
pub fn theme(mut self, t: Theme) -> Self {
self.theme = t;
self
}
}
pub fn bode(omegas: &[f64], mag_db: &[f64], phase_deg: &[f64], opts: BodeOpts) -> String {
let xlim = match (
omegas.iter().copied().fold(f64::INFINITY, f64::min),
omegas.iter().copied().fold(f64::NEG_INFINITY, f64::max),
) {
(lo, hi) if lo.is_finite() && hi.is_finite() => (lo, hi),
_ => (1.0, 10.0),
};
let mag_fig = Figure::new()
.dimensions(opts.cell_width, opts.cell_height)
.theme(opts.theme.clone())
.title(opts.title.clone().unwrap_or_default())
.xlabel(opts.xlabel.clone())
.ylabel(opts.mag_label.clone())
.xlog()
.xlim(xlim.0, xlim.1)
.line(omegas, mag_db, |s| s);
let phase_fig = Figure::new()
.dimensions(opts.cell_width, opts.cell_height)
.theme(opts.theme)
.xlabel(opts.xlabel)
.ylabel(opts.phase_label)
.xlog()
.xlim(xlim.0, xlim.1)
.line(omegas, phase_deg, |s| s);
multi_panel(
&[mag_fig, phase_fig],
PanelGridOpts {
columns: Some(1),
cell_width: Some(opts.cell_width),
cell_height: Some(opts.cell_height),
..PanelGridOpts::default()
},
)
}
#[derive(Debug, Clone)]
pub struct QqOpts {
pub label: Option<String>,
pub reference: bool,
pub marker: crate::markers::Marker,
}
impl Default for QqOpts {
fn default() -> Self {
Self {
label: None,
reference: true,
marker: crate::markers::Marker::CircleOpen,
}
}
}
impl QqOpts {
pub fn label(mut self, s: impl Into<String>) -> Self {
self.label = Some(s.into());
self
}
pub fn no_reference(mut self) -> Self {
self.reference = false;
self
}
pub fn marker(mut self, m: crate::markers::Marker) -> Self {
self.marker = m;
self
}
}
pub fn qq_plot(samples: &[f64], opts: QqOpts) -> Figure {
let mut sorted: Vec<f64> = samples.iter().copied().filter(|v| !v.is_nan()).collect();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let n = sorted.len();
let theoretical: Vec<f64> = (1..=n)
.map(|k| {
let p = (k as f64 - 0.5) / n as f64;
normal_quantile(p)
})
.collect();
let mut fig = Figure::new()
.size(PaperSize::A5Landscape)
.title("Q-Q plot")
.xlabel("theoretical quantile")
.ylabel("sample quantile");
let label = opts.label.clone();
let marker = opts.marker;
fig = fig.scatter(&theoretical, &sorted, |s| {
let s = s.marker(marker);
if let Some(l) = label {
s.label(l)
} else {
s
}
});
if opts.reference {
let mut all: Vec<f64> = theoretical.iter().copied().chain(sorted.iter().copied()).collect();
all.sort_by(|a, b| a.partial_cmp(b).unwrap());
let lo = all.first().copied().unwrap_or(-1.0);
let hi = all.last().copied().unwrap_or(1.0);
fig = fig.line(&[lo, hi], &[lo, hi], |s| s.stroke(Stroke::Dashed));
}
let _ = (Origin::default(), ScaleKind::Linear);
fig
}