Source code for neurokit2.rsp.rsp_simulate

# -*- coding: utf-8 -*-
import numpy as np

from ..misc import check_random_state, check_random_state_children
from ..signal import signal_distort, signal_simulate, signal_smooth


[docs] def rsp_simulate( duration=10, length=None, sampling_rate=1000, noise=0.01, respiratory_rate=15, method="breathmetrics", random_state=None, random_state_distort="spawn", ): """**Simulate a respiratory signal** Generate an artificial (synthetic) respiratory signal of a given duration and rate. Parameters ---------- duration : int Desired length of duration (s). sampling_rate : int The desired sampling rate (in Hz, i.e., samples/second). length : int The desired length of the signal (in samples). noise : float Noise level (amplitude of the laplace noise). respiratory_rate : float Desired number of breath cycles in one minute. method : str The model used to generate the signal. Can be ``"sinusoidal"`` for a simulation based on a trigonometric sine wave that roughly approximates a single respiratory cycle. If ``"breathmetrics"`` (default), will use an advanced model desbribed by `Noto, et al. (2018) <https://github.com/zelanolab/breathmetrics>`_. random_state : None, int, numpy.random.RandomState or numpy.random.Generator Seed for the random number generator. See for ``misc.check_random_state`` for further information. random_state_distort : {'legacy', 'spawn'}, None, int, numpy.random.RandomState or numpy.random.Generator Random state to be used to distort the signal. If ``"legacy"``, use the same random state used to generate the signal (discouraged as it creates dependent random streams). If ``"spawn"``, spawn independent children random number generators from the random_state argument. If any of the other types, generate independent children random number generators from the random_state_distort provided (this allows generating multiple version of the same signal distorted by different random noise realizations). See Also -------- rsp_clean, rsp_findpeaks, signal_rate, rsp_process, rsp_plot Returns ------- array Vector containing the respiratory signal. Examples -------- .. ipython:: python import pandas as pd import neurokit2 as nk rsp1 = nk.rsp_simulate(duration=30, method="sinusoidal") rsp2 = nk.rsp_simulate(duration=30, method="breathmetrics") @savefig p_rsp_simulate1.png scale=100% pd.DataFrame({"RSP_Simple": rsp1, "RSP_Complex": rsp2}).plot(subplots=True) @suppress plt.close() References ---------- * Noto, T., Zhou, G., Schuele, S., Templer, J., & Zelano, C. (2018). Automated analysis of breathing waveforms using BreathMetrics: A respiratory signal processing toolbox. Chemical Senses, 43(8), 583-597. """ # Seed the random generator for reproducible results rng = check_random_state(random_state) random_state_distort = check_random_state_children(random_state, random_state_distort, n_children=1) # Generate number of samples automatically if length is unspecified if length is None: length = duration * sampling_rate if method.lower() in ["sinusoidal", "sinus", "simple"]: rsp = _rsp_simulate_sinusoidal( duration=duration, sampling_rate=sampling_rate, respiratory_rate=respiratory_rate ) else: rsp = _rsp_simulate_breathmetrics( duration=duration, sampling_rate=sampling_rate, respiratory_rate=respiratory_rate, rng=rng, ) rsp = rsp[0:length] # Add random noise if noise > 0: rsp = signal_distort( rsp, sampling_rate=sampling_rate, noise_amplitude=noise, noise_frequency=[5, 10, 100], noise_shape="laplace", random_state=random_state_distort[0], silent=True, ) return rsp
# ============================================================================= # Simple Sinusoidal Model # ============================================================================= def _rsp_simulate_sinusoidal(duration=10, sampling_rate=1000, respiratory_rate=15): """Generate an artificial (synthetic) respiratory signal by trigonometric sine wave that roughly approximates a single respiratory cycle.""" # Generate values along the length of the duration rsp = signal_simulate( duration=duration, sampling_rate=sampling_rate, frequency=respiratory_rate / 60, amplitude=0.5, ) return rsp # ============================================================================= # BreathMetrics Model # ============================================================================= def _rsp_simulate_breathmetrics_original( nCycles=100, sampling_rate=1000, breathing_rate=0.25, average_amplitude=0.5, amplitude_variance=0.1, phase_variance=0.1, inhale_pause_percent=0.3, inhale_pause_avgLength=0.2, inhale_pauseLength_variance=0.5, exhale_pause_percent=0.3, exhale_pause_avgLength=0.2, exhale_pauseLength_variance=0.5, pause_amplitude=0.1, pause_amplitude_variance=0.2, signal_noise=0.1, rng=None, ): """Simulates a recording of human airflow data by appending individually constructed sin waves and pauses in sequence. This is translated from the matlab code available `here. <https://github.com/zelanolab/breathmetrics/blob/master/simulateRespiratoryData.m>`_ by Noto, et al. (2018). Parameters ---------- nCycles : int or float number of breathing cycles to simulate. sampling_rate : int sampling rate. breathing_rate : float average breathing rate. average_amplitude : float average amplitude of inhales and exhales. amplitude_variance: float variance in respiratory amplitudes. phase_variance: float variance in duration of individual breaths. inhale_pause_percent : float percent of inhales followed by a pause. inhale_pause_avgLength : float average length of inhale pauses. inhale_pauseLength_variance : float variance in inhale pause length. exhale_pause_percent : float percent of exhales followed by a pause. exhale_pause_avgLength : float average length of exhale pauses. exhale_pauseLength_variance : float variance in exhale pause length. pause_amplitude : float noise amplitude of pauses. pause_amplitude_variance : float variance in pause noise. signal_noise : float percent of noise saturation in the simulated signal. Returns ---------- signal vector containing breathmetrics simulated rsp signal. """ # Define additional parameters sample_phase = sampling_rate / breathing_rate inhale_pause_phase = np.round(inhale_pause_avgLength * sample_phase).astype(int) exhale_pause_phase = np.round(exhale_pause_avgLength * sample_phase).astype(int) # Normalize variance by average breath amplitude amplitude_variance_normed = average_amplitude * amplitude_variance amplitudes_with_noise = rng.standard_normal(nCycles) * amplitude_variance_normed + average_amplitude amplitudes_with_noise[amplitudes_with_noise < 0] = 0 # Normalize phase by average breath length phase_variance_normed = phase_variance * sample_phase phases_with_noise = np.round(rng.standard_normal(nCycles) * phase_variance_normed + sample_phase).astype(int) phases_with_noise[phases_with_noise < 0] = 0 # Normalize pause lengths by phase and variation inhale_pauseLength_variance_normed = inhale_pause_phase * inhale_pauseLength_variance inhale_pauseLengths_with_noise = np.round( rng.standard_normal(nCycles) * inhale_pauseLength_variance_normed + inhale_pause_phase ).astype(int) inhale_pauseLengths_with_noise[inhale_pauseLengths_with_noise < 0] = 0 exhale_pauseLength_variance_normed = exhale_pause_phase * exhale_pauseLength_variance exhale_pauseLengths_with_noise = np.round( rng.standard_normal(nCycles) * exhale_pauseLength_variance_normed + inhale_pause_phase ).astype(int) # why inhale pause phase? exhale_pauseLengths_with_noise[exhale_pauseLengths_with_noise < 0] = 0 # Normalize pause amplitudes pause_amplitude_variance_normed = pause_amplitude * pause_amplitude_variance # Initialize empty vector to fill with simulated data simulated_respiration = [] # Initialize parameters to save inhale_onsets = np.zeros(nCycles) exhale_onsets = np.zeros(nCycles) inhale_pause_onsets = np.zeros(nCycles) exhale_pause_onsets = np.zeros(nCycles) inhale_lengths = np.zeros(nCycles) inhale_pauseLengths = np.zeros(nCycles) exhale_lengths = np.zeros(nCycles) exhale_pauseLengths = np.zeros(nCycles) inhale_peaks = np.zeros(nCycles) exhale_troughs = np.zeros(nCycles) i = 1 for c in range(nCycles): # Determine length of inhale pause for this cycle if rng.uniform() < inhale_pause_percent: this_inhale_pauseLength = inhale_pauseLengths_with_noise[c] this_inhale_pause = rng.standard_normal(this_inhale_pauseLength) * pause_amplitude_variance_normed this_inhale_pause[this_inhale_pause < 0] = 0 else: this_inhale_pauseLength = 0 this_inhale_pause = [] # Determine length of exhale pause for this cycle if rng.uniform() < exhale_pause_percent: this_exhale_pauseLength = exhale_pauseLengths_with_noise[c] this_exhale_pause = rng.standard_normal(this_exhale_pauseLength) * pause_amplitude_variance_normed this_exhale_pause[this_exhale_pause < 0] = 0 else: this_exhale_pauseLength = 0 this_exhale_pause = [] # Determine length of inhale and exhale for this cycle to main # breathing rate cycle_length = phases_with_noise[c] - (this_inhale_pauseLength + this_exhale_pauseLength) # If pauses are longer than the time alloted for this breath, set them # to 0 so a real breath can be simulated. This will deviate the # statistics from those initialized but is unavaoidable at the current # state if (cycle_length <= 0) or (cycle_length < min(phases_with_noise) / 4): this_inhale_pauseLength = 0 this_inhale_pause = [] this_exhale_pauseLength = 0 this_exhale_pause = [] cycle_length = phases_with_noise[c] - ( this_inhale_pauseLength + this_exhale_pauseLength ) # Compute inhale and exhale for this cycle this_cycle = np.sin(np.linspace(0, 2 * np.pi, cycle_length)) * amplitudes_with_noise[c] half_cycle = np.round(len(this_cycle) / 2).astype(int) this_inhale = this_cycle[0:half_cycle] this_inhale_length = len(this_inhale) this_exhale = this_cycle[half_cycle:] this_exhale_length = len(this_exhale) # Save parameters for checking inhale_lengths[c] = this_inhale_length inhale_pauseLengths[c] = this_inhale_pauseLength exhale_lengths[c] = this_exhale_length exhale_pauseLengths[c] = this_exhale_pauseLength inhale_onsets[c] = i exhale_onsets[c] = i + this_inhale_length + this_inhale_pauseLength if len(this_inhale_pause) > 0: inhale_pause_onsets[c] = i + this_inhale_length else: inhale_pause_onsets[c] = np.nan if len(this_exhale_pause) > 0: exhale_pause_onsets[c] = ( i + this_inhale_length + this_inhale_pauseLength + this_exhale_length ) else: exhale_pause_onsets[c] = np.nan # Compose breath from parameters this_breath = np.hstack([this_inhale, this_inhale_pause, this_exhale, this_exhale_pause]) # Compute max flow for inhale and exhale for this breath max_ID = np.argmax(this_breath) min_ID = np.argmin(this_breath) inhale_peaks[c] = i + max_ID exhale_troughs[c] = i + min_ID # Append breath to simulated resperation vector simulated_respiration = np.hstack([simulated_respiration, this_breath]) i = i + len(this_breath) - 1 # Smooth signal simulated_respiration = signal_smooth( simulated_respiration, kernel="boxzen", size=sampling_rate / 2 ) if signal_noise == 0: signal_noise = 0.0001 noise_vector = rng.uniform(size=simulated_respiration.shape) * average_amplitude simulated_respiration = simulated_respiration * (1 - signal_noise) + noise_vector * signal_noise raw_features = { "Inhale Onsets": inhale_onsets, "Exhale Onsets": exhale_onsets, "Inhale Pause Onsets": inhale_pause_onsets, "Exhale Pause Onsets": exhale_pause_onsets, "Inhale Lengths": inhale_lengths / sampling_rate, "Inhale Pause Lengths": inhale_pauseLengths / sampling_rate, "Exhale Lengths": exhale_lengths / sampling_rate, "Exhale Pause Lengths": exhale_pauseLengths / sampling_rate, "Inhale Peaks": inhale_peaks, "Exhale Troughs": exhale_troughs, } if len(inhale_pauseLengths[inhale_pauseLengths > 0]) > 0: avg_inhale_pauseLength = np.mean(inhale_pauseLengths[inhale_pauseLengths > 0]) else: avg_inhale_pauseLength = 0 if len(exhale_pauseLengths[exhale_pauseLengths > 0]) > 0: avg_exhale_pauseLength = np.mean(exhale_pauseLengths[exhale_pauseLengths > 0]) else: avg_exhale_pauseLength = 0 estimated_breathing_rate = (1 / np.mean(np.diff(inhale_onsets))) * sampling_rate feature_stats = { "Breathing Rate": estimated_breathing_rate, "Average Inhale Length": np.mean(inhale_lengths / sampling_rate), "Average Inhale Pause Length": avg_inhale_pauseLength / sampling_rate, "Average Exhale Length": np.mean(exhale_lengths / sampling_rate), "Average Exhale Pause Length": avg_exhale_pauseLength / sampling_rate, } return simulated_respiration, raw_features, feature_stats def _rsp_simulate_breathmetrics(duration=10, sampling_rate=1000, respiratory_rate=15, rng=None): n_cycles = int(respiratory_rate / 60 * duration) # Loop until it doesn't fail rsp = False while rsp is False: # Generate a longer than necessary signal so it won't be shorter rsp, _, __ = _rsp_simulate_breathmetrics_original( nCycles=int(n_cycles * 1.5), sampling_rate=sampling_rate, breathing_rate=respiratory_rate / 60, signal_noise=0, rng=rng, ) return rsp