pub const FLICKER_SEED_THRESHOLD_SQ: f32 = 10.0;
pub const FLICKER_RMS_TC_SECONDS: f32 = 60.0;
pub const FLICKER_MIN_RMS_GUARD: f32 = 1.0;
pub const FLICKER_HPF_CUTOFF_HZ: f32 = 0.05;
pub const FLICKER_SMOOTH_TC_SECONDS: f32 = 0.3;
pub const FLICKER_PST_MIN_SAMPLES: u32 = 100;
pub const SOS_BW_35HZ: [[f32; 6]; 3] = [
[
6.395_400_3e-12,
1.279_080_05e-11,
6.395_400_3e-12,
1.0,
-1.947_539_3,
9.482_754e-1,
],
[1.0, 2.0, 1.0, 1.0, -1.961_129_5, 9.618_707_3e-1],
[1.0, 2.0, 1.0, 1.0, -1.985_122_7, 9.858_73e-1],
];
pub const SOS_WEIGHTING: [[f32; 6]; 2] = [
[
2.872_127e-5,
5.744_254e-5,
2.872_127e-5,
1.0,
-1.993_591_7,
9.936_432e-1,
],
[
1.0,
-1.998_211,
9.982_111e-1,
1.0,
-1.981_984_5,
9.820_009_5e-1,
],
];
#[derive(Debug, Clone)]
pub struct BiquadChain<const N: usize> {
sos: [[f32; 6]; N],
z1: [f32; N],
z2: [f32; N],
}
impl<const N: usize> BiquadChain<N> {
pub fn new(sos: [[f32; 6]; N]) -> Self {
Self {
sos,
z1: [0.0; N],
z2: [0.0; N],
}
}
pub fn process(&mut self, input: f32) -> f32 {
let mut x = input;
for i in 0..N {
let b0 = self.sos[i][0];
let b1 = self.sos[i][1];
let b2 = self.sos[i][2];
let a1 = self.sos[i][4];
let a2 = self.sos[i][5];
let y = b0 * x + self.z1[i];
self.z1[i] = b1 * x - a1 * y + self.z2[i];
self.z2[i] = b2 * x - a2 * y;
x = y;
}
x
}
}
#[derive(Debug, Clone)]
pub struct FlickerMeter {
avg_rms: f32,
initialized: bool,
b3_hp_prev_in: f32,
b3_hp_prev_out: f32,
bw_filter: BiquadChain<3>,
wt_filter: BiquadChain<2>,
b4_smooth_prev: f32,
pub p_inst: f32,
pub pst_classifier: PstClassifier,
}
impl Default for FlickerMeter {
fn default() -> Self {
Self::new()
}
}
impl FlickerMeter {
pub fn new() -> Self {
Self {
avg_rms: 230.0,
initialized: false,
b3_hp_prev_in: 0.0,
b3_hp_prev_out: 0.0,
bw_filter: BiquadChain::new(SOS_BW_35HZ),
wt_filter: BiquadChain::new(SOS_WEIGHTING),
b4_smooth_prev: 0.0,
p_inst: 0.0,
pst_classifier: PstClassifier::default(),
}
}
pub fn set_nominal_voltage(&mut self, nominal_v: f32) {
if !self.initialized {
self.avg_rms = nominal_v * nominal_v;
}
}
pub fn process_sample(&mut self, v_in: f32, fs: f32) {
let v_sq = v_in * v_in;
if !self.initialized && v_sq > FLICKER_SEED_THRESHOLD_SQ {
self.avg_rms = v_sq * 0.5;
self.initialized = true;
}
let alpha_rms = 1.0 / (fs * FLICKER_RMS_TC_SECONDS);
self.avg_rms = self.avg_rms * (1.0 - alpha_rms) + v_sq * alpha_rms;
let v_rms = crate::math::sqrt(self.avg_rms).max(FLICKER_MIN_RMS_GUARD);
let v_pu = v_in / (v_rms * core::f32::consts::SQRT_2);
let v_demod = v_pu * v_pu;
let rc_hp = 1.0 / (2.0 * core::f32::consts::PI * FLICKER_HPF_CUTOFF_HZ);
let alpha_hp = rc_hp / (rc_hp + 1.0 / fs);
let b3_hp_out = alpha_hp * (self.b3_hp_prev_out + v_demod - self.b3_hp_prev_in);
self.b3_hp_prev_in = v_demod;
self.b3_hp_prev_out = b3_hp_out;
let bw_out = self.bw_filter.process(b3_hp_out);
let wt_out = self.wt_filter.process(bw_out);
let block4_in = wt_out * wt_out;
let alpha_smooth = (1.0 / fs) / (FLICKER_SMOOTH_TC_SECONDS + 1.0 / fs);
let b4_out = self.b4_smooth_prev + alpha_smooth * (block4_in - self.b4_smooth_prev);
self.b4_smooth_prev = b4_out;
self.p_inst = b4_out;
self.pst_classifier.add_sample(b4_out);
}
pub fn calculate_pst(&self) -> f32 {
self.pst_classifier.calculate_pst()
}
pub fn reset_pst(&mut self) {
self.pst_classifier.reset();
}
}
pub const FLICKER_BINS: usize = 64;
pub const FLICKER_MIN_P: f32 = 0.001;
pub const FLICKER_MAX_P: f32 = 100.0;
#[derive(Debug, Clone, Copy)]
pub struct PstClassifier {
pub histogram: [u32; FLICKER_BINS],
pub total_samples: u32,
}
impl Default for PstClassifier {
fn default() -> Self {
Self {
histogram: [0; FLICKER_BINS],
total_samples: 0,
}
}
}
impl PstClassifier {
pub fn reset(&mut self) {
self.histogram = [0; FLICKER_BINS];
self.total_samples = 0;
}
pub fn add_sample(&mut self, p_inst: f32) {
if p_inst <= 0.0 {
return;
}
let clamped = p_inst.clamp(FLICKER_MIN_P, FLICKER_MAX_P);
let norm_log = crate::math::ln(clamped / FLICKER_MIN_P)
/ crate::math::ln(FLICKER_MAX_P / FLICKER_MIN_P);
let bin_idx =
(crate::math::floor(norm_log * FLICKER_BINS as f32) as usize).min(FLICKER_BINS - 1);
self.histogram[bin_idx] += 1;
self.total_samples += 1;
}
fn bin_to_p_inst(bin_idx: usize) -> f32 {
let frac = (bin_idx as f32 + 0.5) / FLICKER_BINS as f32;
FLICKER_MIN_P * crate::math::powf(FLICKER_MAX_P / FLICKER_MIN_P, frac)
}
pub fn get_exceeded_percentile(&self, percent: f32) -> f32 {
if self.total_samples == 0 {
return 0.0;
}
let target_count = crate::math::round(self.total_samples as f32 * (percent / 100.0)) as u32;
let mut accum = 0;
for bin in (0..FLICKER_BINS).rev() {
accum += self.histogram[bin];
if accum >= target_count {
return Self::bin_to_p_inst(bin);
}
}
Self::bin_to_p_inst(0)
}
pub fn calculate_pst(&self) -> f32 {
if self.total_samples < FLICKER_PST_MIN_SAMPLES {
return 0.0;
}
let p_0_1 = self.get_exceeded_percentile(0.1);
let p_1 = self.get_exceeded_percentile(1.0);
let p_3 = self.get_exceeded_percentile(3.0);
let p_10 = self.get_exceeded_percentile(10.0);
let p_50 = self.get_exceeded_percentile(50.0);
let sum_sq = 0.0314 * p_0_1 + 0.0525 * p_1 + 0.0657 * p_3 + 0.2800 * p_10 + 0.0800 * p_50;
crate::math::sqrt(sum_sq.max(0.0))
}
}
pub fn calculate_plt(pst_12_samples: &[f32; 12]) -> f32 {
let mut sum_cube = 0.0;
for &pst in pst_12_samples.iter() {
sum_cube += crate::math::powi(pst.max(0.0), 3);
}
crate::math::cbrt(sum_cube / 12.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plt_calculation() {
let pst_samples = [1.0; 12];
let plt = calculate_plt(&pst_samples);
assert!((plt - 1.0).abs() < 1e-4);
let pst_varying = [1.0, 1.2, 0.8, 1.1, 0.9, 1.0, 1.3, 0.7, 1.0, 1.1, 0.9, 1.0];
let plt_var = calculate_plt(&pst_varying);
assert!(plt_var > 0.9 && plt_var < 1.3);
}
}