imptube.tube

In this module, three main classes are defined - Measurement, Tube and Sample.

  1'''In this module, three main classes are defined - Measurement, Tube and Sample.
  2'''
  3
  4import sys
  5import sounddevice as sd
  6import numpy as np
  7import pandas as pd
  8import soundfile as sf
  9import os
 10from scipy.signal import chirp
 11from scipy.signal.windows import hann
 12from time import sleep, strftime
 13from imptube.utils import make_foldertree
 14from imptube.processing import (
 15    calibration_from_files,
 16    transfer_function_from_path,
 17    alpha_from_path,
 18    harmonic_distortion_filter,
 19    calc_rms_pressure_level
 20)
 21from typing import Protocol
 22import logging
 23
 24
 25class Measurement:
 26    """Contains information about measurement from the perspective of
 27    signal and boundary conditions.
 28
 29    Attributes
 30    ----------
 31    fs : int
 32        measurement sampling frequency
 33    channels_in : list[int]
 34        list of input channel numbers
 35    channels_out : list[int]
 36        list of output channel numbers (usually one member list)
 37    device : str
 38        string specifying part of sound card name
 39        List of available devices can be obtained with
 40        `python3 -m sounddevice` command.
 41    samples : int
 42        number of samples in the generated log sweep
 43        typically 2**n
 44    window_len : int
 45        length of the Hann half-window applied to the ends of the sweep
 46    sub_measurements : int
 47        number of measurements taken for each specimen
 48        Normally, no differences between sweep measurements should occur,
 49        this attribute mainly compensates for potential playback artifacts.
 50    f_low : int
 51        lower frequency limit for the generated sweep
 52    f_high : int
 53        higher frequency limit for the generated sweep
 54    fs_to_spl : float
 55        conversion level from dBFS to dB SPL for microphone 1
 56    sweep_lvl : float
 57        level of the sweep in dBFS
 58    """
 59
 60    def __init__(
 61            self, 
 62            fs : int=48000, 
 63            channels_in : list[int]=[1,2], 
 64            channels_out : list[int]=[1], 
 65            device : str='Scarlett',
 66            samples : int=131072, 
 67            window_len : int=8192,
 68            sub_measurements : int=2,
 69            f_low : int=10,
 70            f_high : int=1000,
 71            fs_to_spl : float=130,
 72            sweep_lvl : float=-6  
 73        ):
 74        self.fs = fs
 75        self.channels_in = channels_in
 76        self.channels_out = channels_out
 77        self.device = device
 78        self.samples = samples
 79        self.window_len = window_len
 80        self.sub_measurements = sub_measurements
 81        self.f_limits = [f_low, f_high]
 82        self.fs_to_spl = fs_to_spl
 83        self.sweep_lvl = sweep_lvl
 84
 85        self.make_sweep()
 86        sd.default.samplerate = fs
 87        sd.default.channels = len(channels_in), len(channels_out)
 88        sd.default.device = device
 89
 90
 91    def make_sweep(self) -> np.ndarray:
 92        """Generates numpy array with log sweep.
 93
 94        Parameters
 95        ----------
 96        fs : int 
 97            measurement sampling frequency
 98        samples : int
 99            number of samples in the generated log sweep
100            typically 2**n
101        window_len : int
102            length of the Hann half-window applied to the ends
103            of the sweep
104        f_low : int
105            lower frequency limit for the generated sweep
106        f_high : int
107            higher frequency limit for the generated sweep
108
109        Returns
110        -------
111        log_sweep : np.ndarray
112            numpy array containing mono log sweep
113        """
114        t = np.linspace(0,self.samples/self.fs,self.samples, dtype=np.float32)
115        
116        half_win = int(self.window_len)
117        log_sweep = chirp(t, self.f_limits[0], t[-1], self.f_limits[1], method="log", phi=90)
118        window = hann(int(self.window_len*2))
119
120        log_sweep[:half_win] = log_sweep[:half_win]*window[:half_win]
121        log_sweep[-half_win:] = log_sweep[-half_win:]*window[half_win:]
122        lvl_to_factor = 10**(self.sweep_lvl/20)
123        log_sweep = log_sweep*lvl_to_factor
124
125        self.sweep = log_sweep
126        return log_sweep
127    
128    def regen_sweep(self):
129        """Regenerates the sweep."""
130        self.make_sweep()
131
132    def measure(self,
133            out_path : str='',
134            thd_filter : bool=True,
135            export : bool=True
136            ) -> tuple[np.ndarray, int]:
137        """Performs measurement and saves the recording. 
138        
139        Parameters
140        ----------
141        out_path : str
142            path where the recording should be saved, including filename
143        thd_filter : bool
144            enables harmonic distortion filtering
145            This affects the files saved.
146        export : bool
147            enables export to specified path
148
149        Returns
150        -------
151        data : np.ndarray
152            measured audio data
153        fs : int
154            sampling rate
155        """
156        data = sd.playrec(
157            self.sweep, 
158            input_mapping=self.channels_in, 
159            output_mapping=self.channels_out)
160        sd.wait()
161        data = np.asarray(data)
162
163        #filtration
164        if thd_filter:
165            data = data.T
166            data = harmonic_distortion_filter(
167                data, 
168                self.sweep, 
169                f_low=self.f_limits[0], 
170                f_high=self.f_limits[1]
171                )
172            data = data.T
173        
174        if export:    
175            sf.write(
176                file=out_path,
177                data=data,
178                samplerate=self.fs,
179                format='WAV',
180                subtype='FLOAT'
181                )
182        
183        return data, self.fs
184
185class Tube:
186    """Class representing tube geometry.
187
188    Attributes
189    ----------
190    further_mic_dist : float
191        further microphone distance from sample
192    closer_mic_dist : float
193        closer mic distance from sample
194    freq_limit : int
195        higher frequency limit for exports
196    """
197    def __init__(self,
198            further_mic_dist : float=0.400115, #x_1
199            closer_mic_dist : float=0.101755, #x_2
200            freq_limit : int=2000,
201            ):
202        self.further_mic_dist = further_mic_dist
203        self.closer_mic_dist = closer_mic_dist
204        self.mic_spacing = further_mic_dist - closer_mic_dist
205        self.freq_limit = freq_limit
206
207class Sample:
208    """A class representing sample and its boundary conditions.
209    
210    Attributes
211    ----------
212    name : str
213        name of the sample
214    temperature : float
215        ambient temperature in degC
216    rel_humidity : float
217        ambient relative humidity in %
218    tube : Tube
219        impedance tube definition object
220    timestamp : str
221        strftime timestamp in a format '%y-%m-%d_%H-%M'
222    folder : str
223        path to project data folder, defaults to "data"
224    """
225    def __init__(self,
226            name : str,
227            temperature : float,
228            rel_humidity : float,
229            atm_pressure : float = 101325,
230            tube : Tube=Tube(),
231            timestamp : str = strftime("%y-%m-%d_%H-%M"),
232            folder = "data",
233            ):
234        self.name = name
235        self.timestamp = timestamp
236        self.temperature = temperature
237        self.atm_pressure = atm_pressure
238        self.rel_humidity = rel_humidity
239        self.tube = tube
240        self.folder = folder
241        self.trees = make_foldertree(
242            self.name, 
243            self.folder, 
244            self.timestamp
245            )
246        bound_dict = {
247            'temp': [self.temperature],
248            'RH': [self.rel_humidity],
249            'atm_pressure': [self.atm_pressure],
250            'x1': [self.tube.further_mic_dist],
251            'x2': [self.tube.closer_mic_dist],
252            'lim': [self.tube.freq_limit],
253            'dbfs_to_spl': [130],
254            }
255        self.boundary_df = pd.DataFrame(bound_dict)
256        self.boundary_df.to_csv(
257            os.path.join(
258                self.trees[2],self.trees[1]+"_bound_cond.csv"
259            )
260        )
261            
262    def migrate_cal(self, cal_name, cal_stamp, cal_parent="data"):
263        """Migrates calibration files from different measurement.
264        
265        Parameters
266        ----------
267        cal_name : str
268            calibration sample name
269        cal_stamp : str
270            calibration sample timestamp i a '%y-%m-%d_%H-%M' format
271        cal_parent : str
272            parent data folder, defaults to 'data'
273        """
274        cal_trees = make_foldertree(
275            variant=cal_name,
276            time_stamp=cal_stamp,
277            parent=cal_parent
278            )
279        cal_parent_folder = cal_trees[2]
280        import_folder = cal_trees[3][1]
281        freqs = np.load(
282            os.path.join(cal_parent_folder, cal_trees[1]+"_freqs.npy")
283            ) #freq import
284        cf = np.load(
285            os.path.join(import_folder, cal_trees[1]+"_cal_f_12.npy")
286            ) #cf import
287
288        parent_folder = self.trees[2]
289        export_folder = self.trees[3][1]
290        np.save(
291            os.path.join(parent_folder, self.trees[1]+"_freqs.npy"),
292            freqs
293            ) #freq export
294        np.save(
295            os.path.join(export_folder, self.trees[1]+"_cal_f_12.npy"),
296            cf
297            ) #cf export
298
299def calibration(
300        sample : Sample,
301        measurement : Measurement,
302        thd_filter = True
303        ):
304    """Performs CLI calibration measurement.
305    
306    Parameters
307    ----------
308
309    sample : imptube.tube.Sample
310        
311    measurement : Measurement
312
313    thd_filter : bool
314        Enables harmonic distortion filtering
315    """
316    caltree = sample.trees[3][0]
317    if not os.path.exists(caltree):
318        os.makedirs(caltree)
319
320    m = measurement
321    running = True
322    while running:
323        for c in range(1, 3):
324            ready = input(f"Calibrate in configuration {c}? [Y/n]")
325            if ready.lower() == "n":
326                break
327            else:
328                for s in range(m.sub_measurements):
329                    f = os.path.join(caltree, sample.trees[1]+f"_cal_wav_conf{c}_{s}.wav")
330                    print(f)
331                    m.measure(f, thd_filter=thd_filter)
332                    sleep(0.5)
333        if input("Repeat calibration process? [y/N]").lower() == "y":
334            continue
335        else:
336            running = False
337        input("Move the microphones to original position before measurement!")
338
339    return calibration_from_files(parent_folder=sample.trees[2])
340
341def single_measurement(
342        sample : Sample,
343        measurement : Measurement,
344        depth : float,
345        thd_filter : bool= True,
346        calc_spl : bool = True
347        ) -> tuple[list[np.ndarray], int]:
348    """Performs measurement.
349    
350    Parameters
351    ----------
352
353    sample : imptube.tube.Sample
354        
355    measurement : Measurement
356
357    depth : float
358        current depth of the sample
359    thd_filter : bool
360        Enables harmonic distortion filtering
361
362    Returns
363    -------
364    sub_measurement_data : list[np.ndarray]
365        list of audio recordings taken
366    fs : float
367        sampling rate of the recording
368    """
369    m = measurement
370    sub_measurement_data = []
371    for s in range(m.sub_measurements):
372        f = os.path.join(sample.trees[4][0], sample.trees[1]+f"_wav_d{depth}_{s}.wav")
373        data, fs = m.measure(f, thd_filter=thd_filter)
374        sub_measurement_data.append(data)
375        sleep(0.5)
376
377    if calc_spl:
378        rms_spl = calc_rms_pressure_level(data.T[0], m.fs_to_spl)
379        logging.info(f"RMS SPL: {rms_spl} dB")
380        m.rms_spl = rms_spl
381    return sub_measurement_data, fs
382
383def calculate_alpha(
384        sample : Sample,
385        return_r : bool = False,
386        return_z : bool = False,
387        ) -> tuple[np.ndarray, np.ndarray]:
388    """Performs transfer function and alpha calculations from audio data
389    found in a valid folder structure.
390
391    Parameters
392    ----------
393    sample : Sample
394
395    Returns
396    -------
397    alpha : np.ndarray
398        sound absorption coefficient for frequencies lower than 
399        limit specified in sample.tube.freq_limit
400    freqs : np.ndarray
401        frequency values for the alpha array
402    """
403    sample.unique_d, sample.tfs = transfer_function_from_path(sample.trees[2])
404    results = alpha_from_path(
405        sample.trees[2],
406        return_f=True,
407        return_r=return_r,
408        return_z=return_z
409        )
410    return results
411
412class Sensor(Protocol):
413    """A protocol for Sensor class implementation."""
414    def read_temperature(self) -> float:
415        ...
416    
417    def read_humidity(self) -> float:
418        ...
419
420    def read_pressure(self) -> float:
421        ...
422    
423def read_env_bc(sensor : Sensor) -> tuple[float, float, float]:
424    for i in range(5):
425        try:
426            temperature = sensor.read_temperature()
427            rel_humidity = sensor.read_humidity()
428            atm_pressure = sensor.read_pressure()
429            break
430        except:
431            print(f"Reading {i+1} not succesful.")
432        if i == 4:
433            print("Unable to read data from sensor, try manually enter temperature and RH on initialization.")
434            sys.exit()
435    return temperature, rel_humidity, atm_pressure
436
437    #  TODO save bc as config file...
438    #  bound_dict = {
439    #     'temp': [self.temperature],
440    #     'RH': [self.RH],
441    #     'x1': [self.x_1],
442    #     'x2': [self.x_2],
443    #     'lim': [self.limit],
444    #     }
445    # self.boundary_df = pd.DataFrame(bound_dict)
446    # self.trees = make_foldertree(self.name, self.folder)
447    # self.boundary_df.to_csv(
448    #     os.path.join(
449    #         self.trees[2],self.trees[1]+"_bound_cond.csv"
450    #     )
451    # )
452    
class Measurement:
 26class Measurement:
 27    """Contains information about measurement from the perspective of
 28    signal and boundary conditions.
 29
 30    Attributes
 31    ----------
 32    fs : int
 33        measurement sampling frequency
 34    channels_in : list[int]
 35        list of input channel numbers
 36    channels_out : list[int]
 37        list of output channel numbers (usually one member list)
 38    device : str
 39        string specifying part of sound card name
 40        List of available devices can be obtained with
 41        `python3 -m sounddevice` command.
 42    samples : int
 43        number of samples in the generated log sweep
 44        typically 2**n
 45    window_len : int
 46        length of the Hann half-window applied to the ends of the sweep
 47    sub_measurements : int
 48        number of measurements taken for each specimen
 49        Normally, no differences between sweep measurements should occur,
 50        this attribute mainly compensates for potential playback artifacts.
 51    f_low : int
 52        lower frequency limit for the generated sweep
 53    f_high : int
 54        higher frequency limit for the generated sweep
 55    fs_to_spl : float
 56        conversion level from dBFS to dB SPL for microphone 1
 57    sweep_lvl : float
 58        level of the sweep in dBFS
 59    """
 60
 61    def __init__(
 62            self, 
 63            fs : int=48000, 
 64            channels_in : list[int]=[1,2], 
 65            channels_out : list[int]=[1], 
 66            device : str='Scarlett',
 67            samples : int=131072, 
 68            window_len : int=8192,
 69            sub_measurements : int=2,
 70            f_low : int=10,
 71            f_high : int=1000,
 72            fs_to_spl : float=130,
 73            sweep_lvl : float=-6  
 74        ):
 75        self.fs = fs
 76        self.channels_in = channels_in
 77        self.channels_out = channels_out
 78        self.device = device
 79        self.samples = samples
 80        self.window_len = window_len
 81        self.sub_measurements = sub_measurements
 82        self.f_limits = [f_low, f_high]
 83        self.fs_to_spl = fs_to_spl
 84        self.sweep_lvl = sweep_lvl
 85
 86        self.make_sweep()
 87        sd.default.samplerate = fs
 88        sd.default.channels = len(channels_in), len(channels_out)
 89        sd.default.device = device
 90
 91
 92    def make_sweep(self) -> np.ndarray:
 93        """Generates numpy array with log sweep.
 94
 95        Parameters
 96        ----------
 97        fs : int 
 98            measurement sampling frequency
 99        samples : int
100            number of samples in the generated log sweep
101            typically 2**n
102        window_len : int
103            length of the Hann half-window applied to the ends
104            of the sweep
105        f_low : int
106            lower frequency limit for the generated sweep
107        f_high : int
108            higher frequency limit for the generated sweep
109
110        Returns
111        -------
112        log_sweep : np.ndarray
113            numpy array containing mono log sweep
114        """
115        t = np.linspace(0,self.samples/self.fs,self.samples, dtype=np.float32)
116        
117        half_win = int(self.window_len)
118        log_sweep = chirp(t, self.f_limits[0], t[-1], self.f_limits[1], method="log", phi=90)
119        window = hann(int(self.window_len*2))
120
121        log_sweep[:half_win] = log_sweep[:half_win]*window[:half_win]
122        log_sweep[-half_win:] = log_sweep[-half_win:]*window[half_win:]
123        lvl_to_factor = 10**(self.sweep_lvl/20)
124        log_sweep = log_sweep*lvl_to_factor
125
126        self.sweep = log_sweep
127        return log_sweep
128    
129    def regen_sweep(self):
130        """Regenerates the sweep."""
131        self.make_sweep()
132
133    def measure(self,
134            out_path : str='',
135            thd_filter : bool=True,
136            export : bool=True
137            ) -> tuple[np.ndarray, int]:
138        """Performs measurement and saves the recording. 
139        
140        Parameters
141        ----------
142        out_path : str
143            path where the recording should be saved, including filename
144        thd_filter : bool
145            enables harmonic distortion filtering
146            This affects the files saved.
147        export : bool
148            enables export to specified path
149
150        Returns
151        -------
152        data : np.ndarray
153            measured audio data
154        fs : int
155            sampling rate
156        """
157        data = sd.playrec(
158            self.sweep, 
159            input_mapping=self.channels_in, 
160            output_mapping=self.channels_out)
161        sd.wait()
162        data = np.asarray(data)
163
164        #filtration
165        if thd_filter:
166            data = data.T
167            data = harmonic_distortion_filter(
168                data, 
169                self.sweep, 
170                f_low=self.f_limits[0], 
171                f_high=self.f_limits[1]
172                )
173            data = data.T
174        
175        if export:    
176            sf.write(
177                file=out_path,
178                data=data,
179                samplerate=self.fs,
180                format='WAV',
181                subtype='FLOAT'
182                )
183        
184        return data, self.fs

Contains information about measurement from the perspective of signal and boundary conditions.

Attributes
  • fs (int): measurement sampling frequency
  • channels_in (list[int]): list of input channel numbers
  • channels_out (list[int]): list of output channel numbers (usually one member list)
  • device (str): string specifying part of sound card name List of available devices can be obtained with python3 -m sounddevice command.
  • samples (int): number of samples in the generated log sweep typically 2**n
  • window_len (int): length of the Hann half-window applied to the ends of the sweep
  • sub_measurements (int): number of measurements taken for each specimen Normally, no differences between sweep measurements should occur, this attribute mainly compensates for potential playback artifacts.
  • f_low (int): lower frequency limit for the generated sweep
  • f_high (int): higher frequency limit for the generated sweep
  • fs_to_spl (float): conversion level from dBFS to dB SPL for microphone 1
  • sweep_lvl (float): level of the sweep in dBFS
def make_sweep(self) -> numpy.ndarray:
 92    def make_sweep(self) -> np.ndarray:
 93        """Generates numpy array with log sweep.
 94
 95        Parameters
 96        ----------
 97        fs : int 
 98            measurement sampling frequency
 99        samples : int
100            number of samples in the generated log sweep
101            typically 2**n
102        window_len : int
103            length of the Hann half-window applied to the ends
104            of the sweep
105        f_low : int
106            lower frequency limit for the generated sweep
107        f_high : int
108            higher frequency limit for the generated sweep
109
110        Returns
111        -------
112        log_sweep : np.ndarray
113            numpy array containing mono log sweep
114        """
115        t = np.linspace(0,self.samples/self.fs,self.samples, dtype=np.float32)
116        
117        half_win = int(self.window_len)
118        log_sweep = chirp(t, self.f_limits[0], t[-1], self.f_limits[1], method="log", phi=90)
119        window = hann(int(self.window_len*2))
120
121        log_sweep[:half_win] = log_sweep[:half_win]*window[:half_win]
122        log_sweep[-half_win:] = log_sweep[-half_win:]*window[half_win:]
123        lvl_to_factor = 10**(self.sweep_lvl/20)
124        log_sweep = log_sweep*lvl_to_factor
125
126        self.sweep = log_sweep
127        return log_sweep

Generates numpy array with log sweep.

Parameters
  • fs (int): measurement sampling frequency
  • samples (int): number of samples in the generated log sweep typically 2**n
  • window_len (int): length of the Hann half-window applied to the ends of the sweep
  • f_low (int): lower frequency limit for the generated sweep
  • f_high (int): higher frequency limit for the generated sweep
Returns
  • log_sweep (np.ndarray): numpy array containing mono log sweep
def regen_sweep(self):
129    def regen_sweep(self):
130        """Regenerates the sweep."""
131        self.make_sweep()

Regenerates the sweep.

def measure( self, out_path: str = '', thd_filter: bool = True, export: bool = True) -> tuple[numpy.ndarray, int]:
133    def measure(self,
134            out_path : str='',
135            thd_filter : bool=True,
136            export : bool=True
137            ) -> tuple[np.ndarray, int]:
138        """Performs measurement and saves the recording. 
139        
140        Parameters
141        ----------
142        out_path : str
143            path where the recording should be saved, including filename
144        thd_filter : bool
145            enables harmonic distortion filtering
146            This affects the files saved.
147        export : bool
148            enables export to specified path
149
150        Returns
151        -------
152        data : np.ndarray
153            measured audio data
154        fs : int
155            sampling rate
156        """
157        data = sd.playrec(
158            self.sweep, 
159            input_mapping=self.channels_in, 
160            output_mapping=self.channels_out)
161        sd.wait()
162        data = np.asarray(data)
163
164        #filtration
165        if thd_filter:
166            data = data.T
167            data = harmonic_distortion_filter(
168                data, 
169                self.sweep, 
170                f_low=self.f_limits[0], 
171                f_high=self.f_limits[1]
172                )
173            data = data.T
174        
175        if export:    
176            sf.write(
177                file=out_path,
178                data=data,
179                samplerate=self.fs,
180                format='WAV',
181                subtype='FLOAT'
182                )
183        
184        return data, self.fs

Performs measurement and saves the recording.

Parameters
  • out_path (str): path where the recording should be saved, including filename
  • thd_filter (bool): enables harmonic distortion filtering This affects the files saved.
  • export (bool): enables export to specified path
Returns
  • data (np.ndarray): measured audio data
  • fs (int): sampling rate
class Tube:
186class Tube:
187    """Class representing tube geometry.
188
189    Attributes
190    ----------
191    further_mic_dist : float
192        further microphone distance from sample
193    closer_mic_dist : float
194        closer mic distance from sample
195    freq_limit : int
196        higher frequency limit for exports
197    """
198    def __init__(self,
199            further_mic_dist : float=0.400115, #x_1
200            closer_mic_dist : float=0.101755, #x_2
201            freq_limit : int=2000,
202            ):
203        self.further_mic_dist = further_mic_dist
204        self.closer_mic_dist = closer_mic_dist
205        self.mic_spacing = further_mic_dist - closer_mic_dist
206        self.freq_limit = freq_limit

Class representing tube geometry.

Attributes
  • further_mic_dist (float): further microphone distance from sample
  • closer_mic_dist (float): closer mic distance from sample
  • freq_limit (int): higher frequency limit for exports
class Sample:
208class Sample:
209    """A class representing sample and its boundary conditions.
210    
211    Attributes
212    ----------
213    name : str
214        name of the sample
215    temperature : float
216        ambient temperature in degC
217    rel_humidity : float
218        ambient relative humidity in %
219    tube : Tube
220        impedance tube definition object
221    timestamp : str
222        strftime timestamp in a format '%y-%m-%d_%H-%M'
223    folder : str
224        path to project data folder, defaults to "data"
225    """
226    def __init__(self,
227            name : str,
228            temperature : float,
229            rel_humidity : float,
230            atm_pressure : float = 101325,
231            tube : Tube=Tube(),
232            timestamp : str = strftime("%y-%m-%d_%H-%M"),
233            folder = "data",
234            ):
235        self.name = name
236        self.timestamp = timestamp
237        self.temperature = temperature
238        self.atm_pressure = atm_pressure
239        self.rel_humidity = rel_humidity
240        self.tube = tube
241        self.folder = folder
242        self.trees = make_foldertree(
243            self.name, 
244            self.folder, 
245            self.timestamp
246            )
247        bound_dict = {
248            'temp': [self.temperature],
249            'RH': [self.rel_humidity],
250            'atm_pressure': [self.atm_pressure],
251            'x1': [self.tube.further_mic_dist],
252            'x2': [self.tube.closer_mic_dist],
253            'lim': [self.tube.freq_limit],
254            'dbfs_to_spl': [130],
255            }
256        self.boundary_df = pd.DataFrame(bound_dict)
257        self.boundary_df.to_csv(
258            os.path.join(
259                self.trees[2],self.trees[1]+"_bound_cond.csv"
260            )
261        )
262            
263    def migrate_cal(self, cal_name, cal_stamp, cal_parent="data"):
264        """Migrates calibration files from different measurement.
265        
266        Parameters
267        ----------
268        cal_name : str
269            calibration sample name
270        cal_stamp : str
271            calibration sample timestamp i a '%y-%m-%d_%H-%M' format
272        cal_parent : str
273            parent data folder, defaults to 'data'
274        """
275        cal_trees = make_foldertree(
276            variant=cal_name,
277            time_stamp=cal_stamp,
278            parent=cal_parent
279            )
280        cal_parent_folder = cal_trees[2]
281        import_folder = cal_trees[3][1]
282        freqs = np.load(
283            os.path.join(cal_parent_folder, cal_trees[1]+"_freqs.npy")
284            ) #freq import
285        cf = np.load(
286            os.path.join(import_folder, cal_trees[1]+"_cal_f_12.npy")
287            ) #cf import
288
289        parent_folder = self.trees[2]
290        export_folder = self.trees[3][1]
291        np.save(
292            os.path.join(parent_folder, self.trees[1]+"_freqs.npy"),
293            freqs
294            ) #freq export
295        np.save(
296            os.path.join(export_folder, self.trees[1]+"_cal_f_12.npy"),
297            cf
298            ) #cf export

A class representing sample and its boundary conditions.

Attributes
  • name (str): name of the sample
  • temperature (float): ambient temperature in degC
  • rel_humidity (float): ambient relative humidity in %
  • tube (Tube): impedance tube definition object
  • timestamp (str): strftime timestamp in a format '%y-%m-%d_%H-%M'
  • folder (str): path to project data folder, defaults to "data"
def migrate_cal(self, cal_name, cal_stamp, cal_parent='data'):
263    def migrate_cal(self, cal_name, cal_stamp, cal_parent="data"):
264        """Migrates calibration files from different measurement.
265        
266        Parameters
267        ----------
268        cal_name : str
269            calibration sample name
270        cal_stamp : str
271            calibration sample timestamp i a '%y-%m-%d_%H-%M' format
272        cal_parent : str
273            parent data folder, defaults to 'data'
274        """
275        cal_trees = make_foldertree(
276            variant=cal_name,
277            time_stamp=cal_stamp,
278            parent=cal_parent
279            )
280        cal_parent_folder = cal_trees[2]
281        import_folder = cal_trees[3][1]
282        freqs = np.load(
283            os.path.join(cal_parent_folder, cal_trees[1]+"_freqs.npy")
284            ) #freq import
285        cf = np.load(
286            os.path.join(import_folder, cal_trees[1]+"_cal_f_12.npy")
287            ) #cf import
288
289        parent_folder = self.trees[2]
290        export_folder = self.trees[3][1]
291        np.save(
292            os.path.join(parent_folder, self.trees[1]+"_freqs.npy"),
293            freqs
294            ) #freq export
295        np.save(
296            os.path.join(export_folder, self.trees[1]+"_cal_f_12.npy"),
297            cf
298            ) #cf export

Migrates calibration files from different measurement.

Parameters
  • cal_name (str): calibration sample name
  • cal_stamp (str): calibration sample timestamp i a '%y-%m-%d_%H-%M' format
  • cal_parent (str): parent data folder, defaults to 'data'
def calibration( sample: Sample, measurement: Measurement, thd_filter=True):
300def calibration(
301        sample : Sample,
302        measurement : Measurement,
303        thd_filter = True
304        ):
305    """Performs CLI calibration measurement.
306    
307    Parameters
308    ----------
309
310    sample : imptube.tube.Sample
311        
312    measurement : Measurement
313
314    thd_filter : bool
315        Enables harmonic distortion filtering
316    """
317    caltree = sample.trees[3][0]
318    if not os.path.exists(caltree):
319        os.makedirs(caltree)
320
321    m = measurement
322    running = True
323    while running:
324        for c in range(1, 3):
325            ready = input(f"Calibrate in configuration {c}? [Y/n]")
326            if ready.lower() == "n":
327                break
328            else:
329                for s in range(m.sub_measurements):
330                    f = os.path.join(caltree, sample.trees[1]+f"_cal_wav_conf{c}_{s}.wav")
331                    print(f)
332                    m.measure(f, thd_filter=thd_filter)
333                    sleep(0.5)
334        if input("Repeat calibration process? [y/N]").lower() == "y":
335            continue
336        else:
337            running = False
338        input("Move the microphones to original position before measurement!")
339
340    return calibration_from_files(parent_folder=sample.trees[2])

Performs CLI calibration measurement.

Parameters
  • sample (Sample):

  • measurement (Measurement):

  • thd_filter (bool): Enables harmonic distortion filtering

def single_measurement( sample: Sample, measurement: Measurement, depth: float, thd_filter: bool = True, calc_spl: bool = True) -> tuple[list[numpy.ndarray], int]:
342def single_measurement(
343        sample : Sample,
344        measurement : Measurement,
345        depth : float,
346        thd_filter : bool= True,
347        calc_spl : bool = True
348        ) -> tuple[list[np.ndarray], int]:
349    """Performs measurement.
350    
351    Parameters
352    ----------
353
354    sample : imptube.tube.Sample
355        
356    measurement : Measurement
357
358    depth : float
359        current depth of the sample
360    thd_filter : bool
361        Enables harmonic distortion filtering
362
363    Returns
364    -------
365    sub_measurement_data : list[np.ndarray]
366        list of audio recordings taken
367    fs : float
368        sampling rate of the recording
369    """
370    m = measurement
371    sub_measurement_data = []
372    for s in range(m.sub_measurements):
373        f = os.path.join(sample.trees[4][0], sample.trees[1]+f"_wav_d{depth}_{s}.wav")
374        data, fs = m.measure(f, thd_filter=thd_filter)
375        sub_measurement_data.append(data)
376        sleep(0.5)
377
378    if calc_spl:
379        rms_spl = calc_rms_pressure_level(data.T[0], m.fs_to_spl)
380        logging.info(f"RMS SPL: {rms_spl} dB")
381        m.rms_spl = rms_spl
382    return sub_measurement_data, fs

Performs measurement.

Parameters
  • sample (Sample):

  • measurement (Measurement):

  • depth (float): current depth of the sample

  • thd_filter (bool): Enables harmonic distortion filtering
Returns
  • sub_measurement_data (list[np.ndarray]): list of audio recordings taken
  • fs (float): sampling rate of the recording
def calculate_alpha( sample: Sample, return_r: bool = False, return_z: bool = False) -> tuple[numpy.ndarray, numpy.ndarray]:
384def calculate_alpha(
385        sample : Sample,
386        return_r : bool = False,
387        return_z : bool = False,
388        ) -> tuple[np.ndarray, np.ndarray]:
389    """Performs transfer function and alpha calculations from audio data
390    found in a valid folder structure.
391
392    Parameters
393    ----------
394    sample : Sample
395
396    Returns
397    -------
398    alpha : np.ndarray
399        sound absorption coefficient for frequencies lower than 
400        limit specified in sample.tube.freq_limit
401    freqs : np.ndarray
402        frequency values for the alpha array
403    """
404    sample.unique_d, sample.tfs = transfer_function_from_path(sample.trees[2])
405    results = alpha_from_path(
406        sample.trees[2],
407        return_f=True,
408        return_r=return_r,
409        return_z=return_z
410        )
411    return results

Performs transfer function and alpha calculations from audio data found in a valid folder structure.

Parameters
  • sample (Sample):
Returns
  • alpha (np.ndarray): sound absorption coefficient for frequencies lower than limit specified in sample.tube.freq_limit
  • freqs (np.ndarray): frequency values for the alpha array
class Sensor(typing.Protocol):
413class Sensor(Protocol):
414    """A protocol for Sensor class implementation."""
415    def read_temperature(self) -> float:
416        ...
417    
418    def read_humidity(self) -> float:
419        ...
420
421    def read_pressure(self) -> float:
422        ...

A protocol for Sensor class implementation.