Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Python cache
__pycache__/
*.pyc

# macOS system files
.DS_Store

# Virtual environments
venv/
.venv/
.logic2/

# IDE / Editor settings
.vscode/

# Logic software files
Logic_Software/squashfs-root
Logic-*

# Project data directories
Temp_Data/
.venv/
Data/
Analysis/
Binary file removed Logic_Software/Logic-1.2.40-Linux.AppImage
Binary file not shown.
Binary file not shown.
Binary file not shown.

This file was deleted.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

This file was deleted.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3 changes: 0 additions & 3 deletions Logic_Software/Logic-1.2.40_WINDOWS/Drivers/Saleae.inf

This file was deleted.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
17 changes: 0 additions & 17 deletions Logic_Software/Logic-1.2.40_WINDOWS/License/License.txt

This file was deleted.

Binary file removed Logic_Software/Logic-1.2.40_WINDOWS/Logic.exe
Binary file not shown.
Binary file removed Logic_Software/Logic-1.2.40_WINDOWS/Qt5Core.dll
Binary file not shown.
Binary file removed Logic_Software/Logic-1.2.40_WINDOWS/Qt5Gui.dll
Binary file not shown.
Binary file removed Logic_Software/Logic-1.2.40_WINDOWS/Qt5Network.dll
Binary file not shown.
Binary file removed Logic_Software/Logic-1.2.40_WINDOWS/Qt5Sql.dll
Binary file not shown.
Binary file removed Logic_Software/Logic-1.2.40_WINDOWS/Qt5Svg.dll
Binary file not shown.
Binary file removed Logic_Software/Logic-1.2.40_WINDOWS/Qt5Widgets.dll
Binary file not shown.
Binary file removed Logic_Software/Logic-1.2.40_WINDOWS/analyzer.dll
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3 changes: 0 additions & 3 deletions Logic_Software/Logic-1.2.40_WINDOWS/imageformats/qsvg.dll

This file was deleted.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed Logic_Software/Logic-1.2.40_WINDOWS/libeay32.dll
Binary file not shown.
Binary file not shown.
3 changes: 0 additions & 3 deletions Logic_Software/Logic-1.2.40_WINDOWS/qt.conf

This file was deleted.

Binary file not shown.
Binary file removed Logic_Software/Logic-1.2.40_WINDOWS/ssleay32.dll
Binary file not shown.
Binary file not shown.
6 changes: 6 additions & 0 deletions logic1_scripts/analyzers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Analyzers package for hydrophone signal processing."""
from .base_analyzer import BaseAnalyzer
from .toa_envelope_analyzer import TOAEnvelopeAnalyzer
from .nearby_analyzer import NearbyAnalyzer

__all__ = ['BaseAnalyzer', 'TOAEnvelopeAnalyzer', 'NearbyAnalyzer']
197 changes: 197 additions & 0 deletions logic1_scripts/analyzers/base_analyzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
"""Base analyzer module for hydrophone signal processing."""
from abc import ABC, abstractmethod
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import butter, sosfilt


class BaseAnalyzer(ABC):
"""Base class for hydrophone signal analyzers.

This abstract class provides common filtering and analysis infrastructure
for different hydrophone signal processing algorithms.
"""

def __init__(
self,
search_band_min: float = 25000,
search_band_max: float = 40000,
filter_order: int = 8,
plot_results: bool = False,
config: dict | None = None
):
"""Initialize analyzer with signal processing parameters.

Args:
search_band_min: Lower frequency bound for analysis (Hz)
search_band_max: Upper frequency bound for analysis (Hz)
filter_order: Order of Butterworth bandpass filter
plot_results: Whether to plot analysis results
config: Optional configuration dictionary
"""
self.search_band_min = search_band_min
self.search_band_max = search_band_max
self.filter_order = filter_order
self.plot_results_flag = plot_results
self.config = config or {}

# ==================== ABSTRACT METHODS ====================

@abstractmethod
def _analyze_single(self, hydrophone, sampling_freq) -> dict:
"""Analyze a single hydrophone signal.

Args:
hydrophone: Hydrophone object with signal data
sampling_freq: Sampling frequency in Hz

Returns:
Dictionary containing analysis results with keys:
- toa_time: Time of arrival (float)
- toa_idx: Index of time of arrival (int)
- filtered_signal: Bandpass filtered signal (np.ndarray)
- processed_signal: Post-processed signal (np.ndarray)
- Additional analyzer-specific fields
"""

@abstractmethod
def _plot_single_signal(self, ax_time, ax_freq, hydrophone, result, idx):
"""Plot analysis results for a single hydrophone.

Args:
ax_time: Matplotlib axis for time domain plot
ax_freq: Matplotlib axis for frequency domain plot
hydrophone: Hydrophone object with signal data
result: Analysis result dictionary from _analyze_single
idx: Hydrophone index
"""

@abstractmethod
def get_name(self) -> str:
"""Return the name of this analyzer.

Returns:
String identifier for the analyzer
"""

# ==================== PUBLIC ====================

def analyze_array(self, hydrophone_array, selected: list[bool] | None = None):
"""Analyze all selected hydrophones in the array.

Args:
hydrophone_array: HydrophoneArray object containing sensor data
selected: List of booleans indicating which hydrophones to analyze

Returns:
Dictionary with keys:
- results: List of individual hydrophone analysis results
- analyzer: Name of the analyzer
"""
if selected is None:
selected = hydrophone_array.selected

# Analyze each hydrophone
results = []
for idx, (hydro, is_selected) in enumerate(
zip(hydrophone_array.hydrophones, selected)
):
if is_selected:
result = self._analyze_single(
hydro, hydrophone_array.sampling_freq
)
result['hydrophone_idx'] = idx
results.append(result)

analysis_results = {
'results': results,
'analyzer': self.get_name()
}

if self.plot_results_flag:
self.plot_results(hydrophone_array, analysis_results, selected)

return analysis_results

def print_results(self, analysis_results):
"""Print analysis results to console.

Args:
analysis_results: Dictionary returned from analyze_array
"""
print(f"\n{analysis_results['analyzer']}")

def plot_results(self, hydrophone_array, analysis_results, selected=None):
"""Plot analysis results for all hydrophones.

Args:
hydrophone_array: HydrophoneArray object containing sensor data
analysis_results: Dictionary returned from analyze_array
selected: List of booleans indicating which hydrophones were analyzed
"""
if selected is None:
selected = hydrophone_array.selected

results = analysis_results['results']
num_plots = len(results)

# Create 2 columns: time domain and frequency domain
_, axes = plt.subplots(
num_plots, 2, figsize=(14, 3*num_plots), squeeze=False
)

for plot_idx, result in enumerate(results):
hydro_idx = result['hydrophone_idx']
hydro = hydrophone_array.hydrophones[hydro_idx]

# Each analyzer defines how to plot ONE signal
self._plot_single_signal(
axes[plot_idx, 0], axes[plot_idx, 1],
hydro, result, hydro_idx
)

# Common formatting
axes[plot_idx, 0].set_ylabel('Amplitude')
axes[plot_idx, 0].set_title(f'Hydrophone {hydro_idx} - Time Domain')
axes[plot_idx, 0].legend(loc='best')
axes[plot_idx, 0].grid(True, alpha=0.3)

axes[plot_idx, 1].set_ylabel('Magnitude')
axes[plot_idx, 1].set_title(f'Hydrophone {hydro_idx} - Frequency Domain')
axes[plot_idx, 1].legend(loc='best')
axes[plot_idx, 1].grid(True, alpha=0.3)

axes[-1, 0].set_xlabel('Time (s)')
axes[-1, 1].set_xlabel('Frequency (Hz)')
plt.tight_layout()
plt.show()

# ==================== COMMON ====================

def apply_bandpass(self, signal, sampling_freq, band_min=None, band_max=None):
"""Apply Butterworth bandpass filter to signal.

Args:
signal: Input signal array
sampling_freq: Sampling frequency in Hz
band_min: Lower frequency bound (uses search_band_min if None)
band_max: Upper frequency bound (uses search_band_max if None)

Returns:
Filtered signal array
"""
if band_min is None:
band_min = self.search_band_min
if band_max is None:
band_max = self.search_band_max

sos = butter(
self.filter_order,
[band_min, band_max],
fs=sampling_freq,
btype='band',
output='sos'
)
return sosfilt(sos, signal)


129 changes: 129 additions & 0 deletions logic1_scripts/analyzers/nearby_analyzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""Nearby detection using static threshold analysis."""
import numpy as np
from scipy.fft import fft, fftfreq

from .base_analyzer import BaseAnalyzer


class NearbyAnalyzer(BaseAnalyzer):
"""Nearby presence detection using static threshold analysis.

This analyzer determines if a signal source is nearby by checking if the
filtered signal exceeds a static amplitude threshold.
"""

def __init__(self, threshold, **kwargs):
"""Initialize nearby analyzer.

Args:
threshold: Static amplitude threshold for nearby detection
**kwargs: Additional arguments passed to BaseAnalyzer
"""
super().__init__(**kwargs)
self.threshold = threshold

def get_name(self):
"""Return analyzer name.

Returns:
String identifier for this analyzer
"""
return "Static Nearby Analyzer"

def print_results(self, analysis_results):
"""Print nearby detection results.

Args:
analysis_results: Dictionary returned from analyze_array
"""
super().print_results(analysis_results)
print(f"\nNearby Detection (threshold: {self.threshold}):")
for result in analysis_results['results']:
status = "NEARBY" if result['nearby'] else "NOT NEARBY"
print(f" Hydrophone {result['hydrophone_idx']}: {status}")

def _analyze_single(self, hydrophone, sampling_freq):
"""Analyze single hydrophone using static threshold.

Args:
hydrophone: Hydrophone object with signal data
sampling_freq: Sampling frequency in Hz

Returns:
Dictionary containing:
- nearby: Boolean indicating if signal exceeds threshold
- filtered_signal: Bandpass filtered signal
- filtered_frequency: FFT of filtered signal
- filtered_freqs: Frequency bins for FFT
- threshold: Detection threshold value
- band_min: Lower frequency bound used (Hz)
- band_max: Upper frequency bound used (Hz)
"""
# Apply bandpass filter
filtered_signal = self.apply_bandpass(
hydrophone.signal, sampling_freq
)

# Detect threshold crossings
toa_candidates = np.where(filtered_signal > self.threshold)[0]
nearby = len(toa_candidates) > 0

# Compute filtered frequency spectrum
filtered_frequency = fft(filtered_signal)
filtered_freqs = fftfreq(len(filtered_signal), 1/sampling_freq)

return {
'nearby': nearby,
'filtered_signal': filtered_signal,
'filtered_frequency': filtered_frequency,
'filtered_freqs': filtered_freqs,
'threshold': self.threshold,
'band_min': self.search_band_min,
'band_max': self.search_band_max
}

def _plot_single_signal(self, ax_time, ax_freq, hydrophone, result, idx):
"""Plot nearby detection results for a single hydrophone.

Args:
ax_time: Matplotlib axis for time domain plot
ax_freq: Matplotlib axis for frequency domain plot
hydrophone: Hydrophone object with signal data
result: Analysis result dictionary from _analyze_single
idx: Hydrophone index
"""
# Time domain plot
ax_time.plot(
hydrophone.times, result['filtered_signal'],
alpha=0.5, label='Filtered Signal', color='blue'
)
ax_time.axhline(
result['threshold'], color='green',
linestyle=':', alpha=0.5, label='Threshold'
)

# Indicate if nearby
status = 'NEARBY' if result['nearby'] else 'NOT NEARBY'
color = 'green' if result['nearby'] else 'red'
ax_time.text(
0.5, 0.95, status,
transform=ax_time.transAxes,
fontsize=12, fontweight='bold',
color=color, ha='center', va='top'
)

# Frequency domain plot
freq_mask = result['filtered_freqs'] >= 0
freqs = result['filtered_freqs'][freq_mask]
magnitude = np.abs(result['filtered_frequency'][freq_mask])

ax_freq.plot(freqs, magnitude, label='Filtered Spectrum', color='blue')
ax_freq.axvline(
result['band_min'], color='red',
linestyle='--', alpha=0.5, label='Filter Range'
)
ax_freq.axvline(
result['band_max'], color='red',
linestyle='--', alpha=0.5
)
ax_freq.set_xlim([0, 100000]) # Focus on relevant frequency range
Loading