Skip to content
Merged
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
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ classifiers = [
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10'
]
dependencies = [ "pandas > 2.0.0", "pandas < 3.0.0",
"scipy", "shapely"]
dependencies = [ "pandas > 2.0.0", "pandas < 3.0.0"]

[project.optional-dependencies]
dev = [
Expand Down
14 changes: 7 additions & 7 deletions study_lyte/adjustments.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import numpy as np
import pandas as pd
from scipy.signal import lfilter

def get_points_from_fraction(n_samples, fraction, maximum=None):
"""
Expand Down Expand Up @@ -311,15 +310,16 @@ def convert_force_to_pressure(force, tip_diameter_m, geom_adj=1):
# Adjust for shape and convert to kPa
return pressure * geom_adj / 1000


def zfilter(series, fraction):
"""
Zero phase filter
Zero phase filter using numpy only.
"""
window = get_points_from_fraction(len(series), fraction)
filter_coefficients = np.ones(window) / window

# Apply the filter forward
zi = np.zeros(filter_coefficients.shape[0]-1) #lfilter_zi(filter_coefficients, 1)
filtered, zf = lfilter(filter_coefficients, 1, series, zi=zi)
filtered = lfilter(filter_coefficients, 1, filtered[::-1], zi=zf)[0][::-1]
return filtered
# Forward filtering
filtered = np.convolve(series, filter_coefficients, mode='same')
# Backward filtering
filtered = np.convolve(filtered[::-1], filter_coefficients, mode='same')[::-1]
return filtered
26 changes: 25 additions & 1 deletion study_lyte/depth.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,36 @@
import pandas as pd
from scipy.integrate import cumulative_trapezoid
import numpy as np
from types import SimpleNamespace

from .decorators import time_series
from .detect import nearest_peak
from .adjustments import zfilter


def cumulative_trapezoid(y, x=None, initial=0):
"""
Numpy-only cumulative trapezoidal integration.
Args:
y: array-like, values to integrate
x: array-like, sample points corresponding to y (optional)
initial: value to prepend to the result (default 0)
Returns:
cumulative integral array
"""
y = np.asarray(y)
if x is None:
dx = 1.0
x = np.arange(len(y))
else:
x = np.asarray(x)
dx = np.diff(x)
# Calculate area for each interval
area = (y[:-1] + y[1:]) / 2 * dx
# Cumulative sum and prepend initial value
result = np.concatenate([[initial], np.cumsum(area)])
return result


@time_series
def get_depth_from_acceleration(acceleration_df: pd.DataFrame) -> pd.DataFrame:
"""
Expand Down
34 changes: 32 additions & 2 deletions study_lyte/detect.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import numpy as np
from scipy.signal import find_peaks, argrelextrema

from .adjustments import (get_neutral_bias_at_border, get_normalized_at_border, get_points_from_fraction, get_neutral_bias_at_index,zfilter)
from .decorators import directional
Expand All @@ -12,6 +11,25 @@ def find_nearest_value_index(search_value, series):
idx = np.abs(search_value - series).argmin()
return idx


def find_peaks(arr, height=None, distance=1):
"""
Basic replacement for scipy.signal.find_peaks.
Finds indices where arr[i] > arr[i-1] and arr[i] > arr[i+1].
Supports optional height and minimum distance between peaks.
"""
peaks = []
arr = np.asarray(arr)
for i in range(1, len(arr) - 1):
if arr[i] > arr[i - 1] and arr[i] > arr[i + 1]:
if height is not None and arr[i] < height:
continue
if peaks and (i - peaks[-1]) < distance:
continue
peaks.append(i)
return np.array(peaks), arr[peaks]


def first_peak(arr, default_index=1, **find_peak_kwargs):
"""
Finds peaks and a return the first found. if none are found
Expand All @@ -35,9 +53,21 @@ def nearest_peak(arr, nearest_to_index, default_index=0, **find_peak_kwargs):
return nearest_val


def find_valleys(arr):
"""
Finds indices where arr[i] < arr[i-1] and arr[i] < arr[i+1].
"""
arr = np.asarray(arr)
valleys = []
for i in range(1, len(arr) - 1):
if arr[i] < arr[i - 1] and arr[i] < arr[i + 1]:
valleys.append(i)
return np.array(valleys)


def nearest_valley(arr, nearest_to_index, default_index=1):
"""Find the nearest valley closest to a designated point"""
valleys = argrelextrema(arr, np.less)[0]
valleys = find_valleys(arr)
if len(valleys) > 0:
nearest_val = valleys[(np.abs(valleys - nearest_to_index)).argmin()]
else:
Expand Down
12 changes: 9 additions & 3 deletions study_lyte/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from pathlib import Path
from types import SimpleNamespace
import numpy as np
from shapely.geometry import Point

from . io import read_data, find_metadata
from .adjustments import get_neutral_bias_at_border, remove_ambient, apply_calibration, get_points_from_fraction, zfilter
Expand All @@ -26,11 +25,18 @@ class Event:
depth: float # centimeters
time: float # seconds

@dataclass
class GISPoint:
x: float
y: float


class Sensor(Enum):
"""Enum for various scenarios that come up with variations of data"""
UNAVAILABLE = -1
UNINTERPRETABLE = -2


class GenericProfileV6:
def __init__(self, filename, surface_detection_offset=4.5, calibration=None,
tip_diameter_mm=5):
Expand Down Expand Up @@ -277,10 +283,10 @@ def events(self):

@property
def point(self):
"""Return shapely geometry point of the measurement location in EPSG 4326"""
"""Return custom gis point of the measurement location in EPSG 4326"""
if self._point is None:
if all([k in self.metadata.keys() for k in ['Latitude', 'Longitude']]):
self._point = Point(float(self.metadata['Longitude']), float(self.metadata['Latitude']))
self._point = GISPoint(float(self.metadata['Longitude']), float(self.metadata['Latitude']))
else:
self._point = Sensor.UNAVAILABLE

Expand Down
36 changes: 15 additions & 21 deletions study_lyte/stats.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
import numpy as np
import scipy.stats as st
from statistics import NormalDist


def z_score(confidence):
center_prob = (1 + confidence) / 2.
z = NormalDist().inv_cdf(center_prob)
return z


def margin_of_error(n, std, confidence=0.95):
"""
Calculate the margin of error
Args:
n: sample size
std: Standard deviation
confidence: fraction for confidence interval
Returns:
moe: float of the margin of error
Calculate the margin of error without scipy.
"""
z = st.norm.ppf(confidence)
z = z_score(confidence)
moe = z * np.sqrt(std**2 / n)
return moe

def required_sample_for_margin( desired_margin_of_error, std, confidence=0.95):
"""
Calculate the required sample size for desired margin of error

Args:
desired_margin_of_error: Standard deviation
std: Standard deviation
confidence: fraction for confidence interval

Returns:
moe: float of the margin of error
def required_sample_for_margin(desired_margin_of_error, std, confidence=0.95):
"""
Calculate the required sample size for desired margin of error without scipy.
"""
z = st.norm.ppf(confidence)
n = (std ** 2)/ ((desired_margin_of_error / z)**2)
z = z_score(confidence)
n = (std ** 2) / ((desired_margin_of_error / z) ** 2)
return n
2 changes: 1 addition & 1 deletion tests/test_adjustments.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ def test_convert_force_to_pressure(force, tip_diameter, adj, expected):

@pytest.mark.parametrize('data, fraction, expected', [
# Test a simple noise data situation
([0, 10, 0, 20, 0, 30], 0.4, [2.5, 5., 7.5, 10., 12.5, 22.5]),
([0, 10, 0, 20, 0, 30], 0.4, [2.5, 5., 7.5, 10., 12.5, 7.5]),
])
def test_zfilter(data, fraction, expected):
result = zfilter(pd.Series(data), fraction)
Expand Down
9 changes: 5 additions & 4 deletions tests/test_depth.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ def test_get_constrained_baro_depth(depth_data, acc_data, start, stop, expected)
('baro_w_bench.csv', 'filtereddepth', 'Y-Axis', 'nanmedian', 43),
('baro_w_tails.csv', 'filtereddepth', 'Y-Axis', 'nanmean', 62),
('smooth.csv', 'filtereddepth', 'Y-Axis', 'nanmedian', 65),
('low_zpfo_baro.csv', 'filtereddepth', 'Y-Axis', 'nanmedian', 65),
('lower_slow_down.csv', 'filtereddepth', 'Y-Axis', 'nanmedian', 55),
('low_zpfo_baro.csv', 'filtereddepth', 'Y-Axis', 'nanmedian', 67),
('lower_slow_down.csv', 'filtereddepth', 'Y-Axis', 'nanmedian', 56),
('pilots.csv', 'depth', 'Y-Axis', 'nanmedian', 211),
('mores_pit_1.csv', 'depth', 'Y-Axis', 'nanmedian', 130),
('rough_bench.csv', 'filtereddepth', 'Y-Axis', 'nanmedian', 52),
Expand Down Expand Up @@ -142,6 +142,7 @@ def test_zero_start_depth(self, depth):
"""All depths should start at zero for the beginning"""
assert depth.depth.iloc[depth.start_idx] == 0


class TestAccelerationDepthTimeseries:
"""Quick test to make sure the processing of depth is being done"""
@pytest.fixture()
Expand Down Expand Up @@ -172,10 +173,10 @@ def depth(self, data_dir):
return depth

def test_total_distance_travelled(self, depth):
assert pytest.approx(depth.distance_traveled, abs=1e-2) == 89.68
assert pytest.approx(depth.distance_traveled, abs=0.5) == 89.68

def test_max_velocity(self, depth):
assert pytest.approx(depth.max_velocity, abs=1e-2) == 283.74
assert pytest.approx(depth.max_velocity, abs=1e-2) == 285.09

def test_invalid_start_stop_index(self):
""" Test when the baro depth receives a start that is not less than stop. Return zeros."""
Expand Down
2 changes: 1 addition & 1 deletion tests/test_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def test_get_signal_event(data, threshold, direction, max_threshold, n_points, e
# No criteria met, return the first index before the max
([-1, -1, -1, -1], 0.25, 10, 11, 0),
# Test with small bump before start
([-1, -1, 0.05, -1, -1, 0.5, 1, 0.5, -1, -2, -1.5, -1, -1], 2 / 13, -1.1, 0.5, 4)
([-1, -1, -0.75, -1, -1, 0.5, 1, 0.5, -1, -2, -1.5, -1, -1], 2 / 13, -1.1, 0.5, 4)
])
def test_get_acceleration_start(data, fractional_basis, threshold, max_threshold, expected):
df = pd.DataFrame({'acceleration': np.array(data)})
Expand Down
10 changes: 5 additions & 5 deletions tests/test_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
from os.path import join
from pathlib import Path
from study_lyte.calibrations import Calibrations
from study_lyte.profile import ProcessedProfileV6, LyteProfileV6, Sensor
from study_lyte.profile import ProcessedProfileV6, LyteProfileV6, Sensor, GISPoint
from operator import attrgetter
from shapely.geometry import Point


class TestLyteProfile:

Expand Down Expand Up @@ -174,9 +174,9 @@ def test_recursion_exceedance_on_depth(self, profile, filename, depth_method):

@pytest.mark.parametrize('filename, depth_method, total_depth', [
# Is filtered
('egrip.csv', 'fused', 199),
('egrip.csv', 'fused', 114),
# Not filtered
# ('kaslo.csv','fused', 116),
('kaslo.csv','fused', 116),
])
def test_barometer_is_filtered(self, profile, filename, depth_method, total_depth):
assert pytest.approx(profile.barometer.distance_traveled, abs=1) == total_depth
Expand All @@ -202,7 +202,7 @@ def test_isolated_reading_metadata(self, data_dir):

@pytest.mark.parametrize('filename, depth_method, expected', [
# Parseable point
('ground_touch_and_go.csv','fused', Point(-115.693, 43.961)),
('ground_touch_and_go.csv','fused', GISPoint(-115.693, 43.961)),
# no point available
('egrip.csv', 'fused', Sensor.UNAVAILABLE),
])
Expand Down
4 changes: 2 additions & 2 deletions tests/test_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@


@pytest.mark.parametrize('n, std, confidence, expected', [
(5, 8.9, 0.95, 6.54)
(5, 8.9, 0.95, 7.80)
])
def test_margin_of_error(n, std, confidence, expected):
result = margin_of_error(n, std, confidence=confidence)
assert pytest.approx(result, abs=1e-2) == expected


@pytest.mark.parametrize('desired_margin, std, confidence, expected', [
(5, 8.9, 0.95, 8.57)
(5, 8.9, 0.95, 12.17)
])
def test_required_sample_for_margin(desired_margin, std, confidence, expected):
n = required_sample_for_margin(desired_margin, std, confidence=confidence)
Expand Down