Lowpass filtering
The Butter lowpass filter with SOS coefficients are used for lowpass filtering with the following implementation:
def butter_lowpass(cutoff: float, fs: float, order: int = 5) -> np.ndarray:
"""
:param cutoff: The cutoff frequency of the filter.
:param fs: The sampling frequency of the signal.
:param order: The order of the filter.
"""
return scipy.signal.butter(
order, cutoff, fs=fs, btype="low", analog=False, output="sos"
)
def butter_lowpass_filter(
data: np.ndarray | pd.Series, cutoff: int, fs: float, order: int
) -> np.ndarray:
"""
:param data: The data to filter.
:param cutoff: The cutoff frequency of the filter.
:param fs: The sampling frequency of the signal.
:param order: The order of the filter.
"""
sos = butter_lowpass(cutoff, fs, order)
y = scipy.signal.sosfiltfilt(sos, data)
return yLowpass filter design
For fixed downsampling rate of 20 (i.e. 50 Hz for original sampling frequency of 1 kHz), we can set the cutoff frequency to the wiki:Nyquist frequency at 25 Hz and do not apply the bandstop filters, since higher frequencies will not be present in the downsampled signal anyways
However, setting the downsampling too high we see significant degradation in the current/field phase space when plotting reference current vs measured field, which is the case when we are predicting using programmed current.

Implementing filters
As of 2024-11-15 new filters are developed for filtering measured current and field from the B-Train, and implemented in hysteresis-scripts using
class BandstopFilter(typing.TypedDict):
f: float
Q: float
class LowPassFilter(typing.TypedDict):
cutoff: float
order: int
class Filters(typing.TypedDict):
bandstop: list[BandstopFilter]
low_pass: LowPassFilter
def combine_filters(filters: Filters, fs: float) -> np.ndarray:
soss = []
for f in filters["bandstop"]:
soss.append(iirnotch(f["f"], f["Q"], fs))
b, a = butter_lowpass(filters["low_pass"]["cutoff"], fs, filters["low_pass"]["order"])
soss.append(scipy.signal.tf2sos(b, a))
return np.concatenate(soss)
def filter_signal(signal: np.ndarray, fs: float, filters: Filters) -> np.ndarray:
for bandstop in filters["bandstop"]:
b, a = scipy.signal.iirnotch(bandstop["f"], bandstop["Q"], fs)
signal = scipy.signal.filtfilt(b, a, signal)
b, a = butter_lowpass(filters["low_pass"]["cutoff"], fs, filters["low_pass"]["order"])
signal = scipy.signal.filtfilt(b, a, signal)
return signal
Through attentuating specific frequencies using bandstop frequencies, with peaks detected using scipy peak-detection algorithm, we achieve field measurements with peak-to-peak ripples in the 6e-6 T range

Filter configurations
Measured current
bandstop:
- Q: 50
f: 50.03
- Q: 20
f: 65.0
- Q: 100
f: 76.43
- Q: 100
f: 83.55
- Q: 50
f: 100.066
fs: 1000.0
low_pass:
cutoff: 25
order: 1Measured field (B-Train)
bandstop:
- Q: 100
f: 31.34
- Q: 100
f: 50.03
- Q: 100
f: 83.59
- Q: 50
f: 100.32
fs: 1000.0
low_pass:
cutoff: 25
order: 1Measured field time derivative (B-Train)
bandstop:
- Q: 50
f: 50.03
- Q: 50
f: 65.0
- Q: 100
f: 76.43
- Q: 100
f: 83.55
- Q: 50
f: 100.066
- Q: 100
f: 150.05
fs: 1000.0
low_pass:
cutoff: 50
order: 1Other filter alternatives
SIAC filter
An alternative is the SIAC filter
https://www.sciencedirect.com/science/article/pii/S0377042715004185
where the code has been given by Prof. J. Ryan at KTH.
- Investigate usage of SIAC filter for MBI data [priority:: medium] [completion:: 2024-08-06]
Does not seem to work for non-fourier data…