`welch` averages the spectra of overlapping segments, which is steadier; `fft` takes the whole run as one segment, which is sharper and noisier.
window
string
hann
hann | hamming | blackman | rectangular
Taper applied to each segment. It stops a peak from smearing across the spectrum; `rectangular` keeps the samples as they are.
axis
int
-1
-8 … 7
Which axis holds the samples. -1 is time.
welch
Name
Type
Default
Range
Doc
unit
string
fraction
fraction | seconds | samples | seconds (ufreq)
What `segment` and `overlap` count in. `fraction` is a share of the frame.
segment
float
0.4
0 … 1
How long one segment is. A longer segment tells frequencies apart better.
overlap
float
0.3
0 … 1
How much of a segment the next one repeats, in `unit`. More overlap is steadier and slower; at a full segment the hop is one sample.
range
Name
Type
Default
Range
Doc
low
float
0
0 … 10000
The lowest frequency to keep, in Hz.
high
float
0
0 … 10000
The highest frequency to keep, in Hz. 0 keeps every bin above `low`.
scale
string
linear
linear | log
Whether the power comes out as it is, or as its logarithm.
common
Name
Type
Default
Range
Doc
autotrigger
bool
false
Run on the node's own schedule, instead of waiting for an input frame. Turn this on for sources; leave it off for transforms driven by their input.
max_frequency
float
0
0 … 100
Rate cap for this node, read through `frequency_mode`. 0 means uncapped — the node runs as often as the scheduler and its inputs allow.
frequency_mode
string
updates_per_second
updates_per_second | seconds_per_update
How to read `max_frequency`: as a rate in Hz (updates per second), or as a period in seconds between updates — convenient for very slow nodes.
Source
The current source of this node, as it stands in the goofi repository at node-bundles/signal/Psd.rs.
node-bundles/signal/Psd.rs
//! Psd — how much power sits at each frequency. Welch averages the spectra of overlapping//! segments, which trades resolution for a steadier answer; `fft` takes one segment, the whole run.usestd::sync::Arc;usegoofi_core::{resolve_axis,stream,Axis,Coord,Data,SlotType};usegoofi_signal_sdk::{Inputs,Manifest,Node,NodeCtx,NodeResult,OutputDecl,Outputs,ParamDecl,Params,ParamSpec,SlotDecl,Tag};userustfft::{num_complex::Complex32,FftPlanner};structPsd{planner:FftPlanner<f32>,}implDefaultforPsd{fndefault()->Psd{Psd{planner:FftPlanner::new()}}}/// A periodic cosine window: `a0 - a1·cos(x) + a2·cos(2x)`, which covers the three on offer.fnwindow(kind:&str,n:usize)->Vec<f32>{let(a0,a1,a2)=matchkind{"hamming"=>(0.54,0.46,0.0),"blackman"=>(0.42,0.5,0.08),"hann"=>(0.5,0.5,0.0),_=>returnvec![1.0;n],};(0..n).map(|i|{letx=std::f64::consts::TAU*iasf64/nasf64;(a0-a1*x.cos()+a2*(2.0*x).cos())asf32}).collect()}implNodeforPsd{fnprocess(&mutself,inp:&Inputs<'_>,out:&mutOutputs<'_>,_c:&mutNodeCtx,p:&Params<'_>,)->NodeResult{letd=inp.get("input").ok_or("`input` is required")?;leta=d.assert_ndims().at_least(1)?;letdim=resolve_axis(p.i64("psd","axis").unwrap_or(-1),a.shape().len())?;letn=a.shape()[dim];ifn<2{returnErr(format!("needs at least 2 samples along the axis, got {n}").into());}letsfreq=d.meta().sfreq().ok_or("this node needs a frame that carries its sample rate")?;lettaper=p.str("psd","window").unwrap_or("hann");// Segment and overlap share one unit, so both are read through the same conversion.letunit=p.str("welch","unit").unwrap_or("fraction");letsamples=|size:f64|->Result<f64,String>{Ok(matchunit{"samples"=>size,"fraction"=>size*nasf64,"seconds (ufreq)"=>goofi_core::stream::window_count(size,"seconds (ufreq)",d.meta())? asf64,_=>size*sfreq,})};let(seg,hop)=ifp.str("psd","mode").unwrap_or("welch")=="fft"{(n,n)}else{letseg=(samples(p.f64("welch","segment").unwrap_or(0.4))?.round().max(2.0)asusize).min(n);letoverlap=samples(p.f64("welch","overlap").unwrap_or(0.3))?.round().max(0.0)asusize;(seg,seg.saturating_sub(overlap).clamp(1,seg))};letbins=seg/2+1;lettaper=window(taper,seg);// Dividing by `sfreq · Σw²` makes the result a DENSITY: the segment length cannot move a peak.letnorm=1.0/(sfreqasf32*taper.iter().map(|w|w*w).sum::<f32>());letfft=self.planner.plan_fft_forward(seg);letfreqs:Vec<f64>=(0..bins).map(|k|kasf64*sfreq/segasf64).collect();letlow=p.f64("range","low").unwrap_or(0.0);lethigh=p.f64("range","high").unwrap_or(0.0);letkept:Vec<usize>=(0..bins).filter(|k|freqs[*k]>=low&&(high<=0.0||freqs[*k]<=high)).collect();ifkept.is_empty(){returnErr(format!("no bin falls between {low} Hz and {high} Hz").into());}letlog=p.str("range","scale").unwrap_or("linear")=="log";letmutscratch=vec![Complex32::default();seg];letspectra:Vec<Vec<f32>>=stream::lanes(a.shape(),dim,a.as_bytes()).iter().map(|lane|{letmutsum=vec![0.0f32;bins];letmutcount=0.0f32;forstartin(0..=n-seg).step_by(hop){for(c,(x,w))inscratch.iter_mut().zip(lane[start..].iter().zip(&taper)){*c=Complex32::new(x*w,0.0);}fft.process(&mutscratch);for(k,(s,c))insum.iter_mut().zip(&scratch[..bins]).enumerate(){// DC and, for an even length, Nyquist have no twin to fold in.letfold=ifk==0||2*k==seg{1.0}else{2.0};*s+=c.norm_sqr()*norm*fold;}count+=1.0;}kept.iter().map(|k|{letv=sum[*k]/count;iflog{v.max(1e-12).ln()}else{v}}).collect()}).collect();letmutshape_out=a.shape().to_vec();shape_out[dim]=kept.len();letcoords:Vec<Coord>=kept.iter().map(|k|Coord::Num(freqs[*k])).collect();// No longer a time series: `sfreq` would read as the spacing of a domain that is gone.letmutmeta=d.meta().clone();letaxes=meta.channels().clone().with(dim,Axis::coords(Arc::from(coords)));meta.set_channels(axes);meta.set_sfreq(None);letbuf=stream::unlanes(&shape_out,dim,&spectra);out.set("out",Data::array_f32(shape_out,buf,meta).map_err(|e|e.to_string())?);Ok(())}}staticPARAMS:&[ParamDecl]=&[ParamDecl{group:"psd",name:"mode",spec:ParamSpec::Str{default:"welch",options:&["fft","welch"],refresh:false},expression:None,doc:Some("`welch` averages the spectra of overlapping segments, which is steadier; `fft` takes \
the whole run as one segment, which is sharper and noisier.",),section:0,show:None,},ParamDecl{group:"psd",name:"window",spec:ParamSpec::Str{default:"hann",options:&["hann","hamming","blackman","rectangular"],refresh:false,},expression:None,doc:Some("Taper applied to each segment. It stops a peak from smearing across the spectrum; \
`rectangular` keeps the samples as they are.",),section:0,show:None,},ParamDecl{group:"psd",name:"axis",spec:ParamSpec::Int{default:-1,min:-8,max:7,options:&[-2,-1,0,1,2]},expression:None,doc:Some("Which axis holds the samples. -1 is time."),section:0,show:None,},ParamDecl{group:"welch",name:"unit",spec:ParamSpec::Str{default:"fraction",options:&["fraction","seconds","samples","seconds (ufreq)"],refresh:false,},expression:None,doc:Some("What `segment` and `overlap` count in. `fraction` is a share of the frame."),section:0,show:None,},ParamDecl{group:"welch",name:"segment",spec:ParamSpec::Float{default:0.4,min:0.0,max:1.0},expression:None,doc:Some("How long one segment is. A longer segment tells frequencies apart better."),section:0,show:None,},ParamDecl{group:"welch",name:"overlap",spec:ParamSpec::Float{default:0.3,min:0.0,max:1.0},expression:None,doc:Some("How much of a segment the next one repeats, in `unit`. More overlap is steadier and \
slower; at a full segment the hop is one sample.",),section:0,show:None,},ParamDecl{group:"range",name:"low",spec:ParamSpec::Float{default:0.0,min:0.0,max:10_000.0},expression:None,doc:Some("The lowest frequency to keep, in Hz."),section:0,show:None,},ParamDecl{group:"range",name:"high",spec:ParamSpec::Float{default:0.0,min:0.0,max:10_000.0},expression:None,doc:Some("The highest frequency to keep, in Hz. 0 keeps every bin above `low`."),section:0,show:None,},ParamDecl{group:"range",name:"scale",spec:ParamSpec::Str{default:"linear",options:&["linear","log"],refresh:false},expression:None,doc:Some("Whether the power comes out as it is, or as its logarithm."),section:0,show:None,},];staticINPUTS:&[SlotDecl]=&[SlotDecl{name:"input",kind:SlotType::Array,trigger_process:true,multi:false,required:true,}];staticOUTPUTS:&[OutputDecl]=&[OutputDecl{name:"out",kind:SlotType::Array}];staticMANIFEST:Manifest=Manifest{tags:&[Tag::Analysis],doc:"How much power sits at each frequency, in Hz, over the last axis.",inputs:INPUTS,outputs:OUTPUTS,params:PARAMS,producer:false,};goofi_signal_sdk::export!(Psd,MANIFEST);