Source code for piel.analysis.signals.time.core.compose
# compose_pulses.py
import numpy as np
from typing import List, Optional
from piel.types import TimeSignalData
from .off_state import (
extract_off_state_generator_from_full_state_data,
) # Adjust the import path as needed
[docs]
def compose_pulses_into_signal(
pulses: List[TimeSignalData],
baseline: float = 0.0,
noise_std: Optional[float] = None,
data_time_signal_kwargs: Optional[dict] = None,
start_time_s: Optional[float] = None,
end_time_s: Optional[float] = None,
) -> TimeSignalData:
"""
Composes a full signal from a list of pulses by inserting them into a continuous time array
and filling gaps with generated noise.
Parameters:
pulses (List[TimeSignalData]): List of pulse signals to be inserted.
baseline (float, optional): Baseline value of the signal. Defaults to 0.0.
noise_std (float, optional): Standard deviation of the noise to be generated in gaps.
If not provided, it is estimated from the pulses.
data_time_signal_kwargs (dict, optional): Additional keyword arguments for DataTimeSignalData.
start_time_s (float, optional): Start time of the composed signal. If not provided, uses the first pulse's start time.
end_time_s (float, optional): End time of the composed signal. If not provided, uses the last pulse's end time.
Returns:
TimeSignalData: The composed full signal with pulses and noise.
"""
if data_time_signal_kwargs is None:
data_time_signal_kwargs = {}
if not pulses:
raise ValueError("The list of pulses is empty.")
# Sort pulses by their start time
pulses_sorted = sorted(pulses, key=lambda pulse: pulse.time_s[0])
# Determine the overall time range
pulse_start_times = [pulse.time_s[0] for pulse in pulses_sorted]
pulse_end_times = [pulse.time_s[-1] for pulse in pulses_sorted]
# Set start_time and end_time based on parameters or pulses
if start_time_s is not None:
if start_time_s > min(pulse_start_times):
raise ValueError("start_time_s is after the start time of some pulses.")
start_time = start_time_s
else:
start_time = min(pulse_start_times)
if end_time_s is not None:
if end_time_s < max(pulse_end_times):
raise ValueError("end_time_s is before the end time of some pulses.")
end_time = end_time_s
else:
end_time = max(pulse_end_times)
# Determine the sampling rate from the first pulse
first_pulse = pulses_sorted[0]
if len(first_pulse.time_s) < 2:
raise ValueError(
"Each pulse must contain at least two time points to determine sampling rate."
)
sampling_intervals = np.diff(first_pulse.time_s)
mean_sampling_interval = np.mean(sampling_intervals)
sampling_rate = 1.0 / mean_sampling_interval
# Create the full time array
full_time_s = np.arange(
start_time, end_time + mean_sampling_interval, mean_sampling_interval
)
# Initialize the full data array with noise
if noise_std is None:
# Estimate noise_std from pulses by assuming pulses are signal and noise is lower
# Take the minimum standard deviation across all pulses
noise_std_estimates = [
np.std(pulse.data) for pulse in pulses_sorted if len(pulse.data) > 1
]
if not noise_std_estimates:
raise ValueError(
"Cannot estimate noise standard deviation from the provided pulses."
)
noise_std = min(noise_std_estimates)
# Generate noise for the entire duration using the defined function
# Create a dummy pulse to initialize the noise generator
dummy_pulse = pulses[0]
noise_generator = extract_off_state_generator_from_full_state_data(
dummy_pulse,
baseline=baseline,
sampling_rate=sampling_rate,
)
# Generate noise for the entire duration
noise_segment = noise_generator(end_time - start_time)
noise_data = np.array(noise_segment.data)
# **Modified Section Starts Here**
# Ensure that the generated noise matches the full_time_s length
if len(noise_data) < len(full_time_s):
pad_length = len(full_time_s) - len(noise_data)
additional_time_s = pad_length * mean_sampling_interval
# Generate additional noise using the noise generator to match the original noise style
additional_noise_segment = noise_generator(
additional_time_s, num_samples=pad_length
)
additional_noise_data = np.array(additional_noise_segment.data)
# Handle cases where the noise generator might return more data than needed
if len(additional_noise_data) < pad_length:
raise ValueError(
f"Noise generator could not generate enough data for padding. "
f"Required: {pad_length}, Generated: {len(additional_noise_data)}"
)
# Truncate to the required pad length
additional_noise_data = additional_noise_data[:pad_length]
# Concatenate the additional noise to the original noise_data
noise_data = np.concatenate([noise_data, additional_noise_data])
elif len(noise_data) > len(full_time_s):
noise_data = noise_data[: len(full_time_s)]
# **Modified Section Ends Here**
full_data = noise_data.copy()
# Implement verification to ensure pulses do not overlap
for i in range(1, len(pulses_sorted)):
previous_pulse_end = pulses_sorted[i - 1].time_s[-1]
current_pulse_start = pulses_sorted[i].time_s[0]
if current_pulse_start < previous_pulse_end:
raise ValueError(f"Pulses at index {i-1} and {i} are overlapping.")
# Insert each pulse into the full data array at the correct indices
for pulse in pulses_sorted:
pulse_time_s = np.array(pulse.time_s)
pulse_data = np.array(pulse.data)
# Find the start and end indices of the pulse in the full_time_s array
pulse_start_time = pulse_time_s[0]
# pulse_end_time = pulse_time_s[-1]
# Calculate the corresponding indices
pulse_start_idx = int(np.round((pulse_start_time - start_time) * sampling_rate))
pulse_end_idx = pulse_start_idx + len(pulse_data)
# Handle boundary conditions
if pulse_start_idx < 0:
raise ValueError(
f"Pulse start time {pulse_start_time} is before the full signal start time {start_time}."
)
if pulse_end_idx > len(full_data):
# Trim the pulse data if it exceeds the full signal end time
pulse_data = pulse_data[: len(full_data) - pulse_start_idx]
pulse_end_idx = len(full_data)
# Insert the pulse data
full_data[pulse_start_idx:pulse_end_idx] = pulse_data
# Create the composed TimeSignalData instance
composed_signal = TimeSignalData(
time_s=full_time_s.tolist(),
data=full_data.tolist(),
data_name="ComposedSignal",
**data_time_signal_kwargs,
)
return composed_signal