From 74d951654130c43b14e267d3a91b30d4386c9356 Mon Sep 17 00:00:00 2001 From: micah johnson Date: Fri, 23 Jan 2026 21:38:43 -0700 Subject: [PATCH 1/2] Working towards a better depth interpretation and a calibration --- study_lyte/adjustments.py | 5 +- study_lyte/profile.py | 104 ++++++++++++++++++++++---------------- 2 files changed, 64 insertions(+), 45 deletions(-) diff --git a/study_lyte/adjustments.py b/study_lyte/adjustments.py index d29675a..ff626e0 100644 --- a/study_lyte/adjustments.py +++ b/study_lyte/adjustments.py @@ -175,12 +175,15 @@ def remove_ambient(active, ambient, min_ambient_range=100, direction='forward'): return clean -def apply_calibration(series, coefficients, minimum=None, maximum=None): +def apply_calibration(series, coefficients, minimum=None, maximum=None, tare=False): """ Apply any calibration using poly1d """ poly = np.poly1d(coefficients) result = poly(series) + if tare: + result = result - np.nanmedian(result[0:50]) + if maximum is not None: result[result > maximum] = maximum if minimum is not None: diff --git a/study_lyte/profile.py b/study_lyte/profile.py index 60a3e4f..72a7259 100644 --- a/study_lyte/profile.py +++ b/study_lyte/profile.py @@ -81,11 +81,12 @@ def __init__(self, filename, surface_detection_offset=4.5, calibration=None, self._error = None self._ground = None - def assign_event_depths(self): + def assign_event_depths(self, depth:pd.Series): """" Enable depth assignment post depth realization """ self.events - for event in [self._start, self._stop, self._surface.nir, self._surface.force]: - event.depth = self.depth.iloc[event.index] + for event in [self._start, self._stop]: + event.depth = depth.iloc[event.index] + self._surface = self.assign_surface_depths(depth) @property def serial_number(self): @@ -178,8 +179,7 @@ def force(self): force = self.raw['Sensor1'].values if self.calibration is not None: if 'Sensor1' in self.calibration.keys(): - force = apply_calibration(self.raw['Sensor1'].values, self.calibration['Sensor1'], minimum=0, maximum=15000) - force = force - np.nanmean(force[0:20]) + force = apply_calibration(self.raw['Sensor1'].values, self.calibration['Sensor1'], minimum=None, maximum=15000, tare=True) self._force = pd.DataFrame({'force': force, 'depth': self.depth.values}) # prefer a ground index if available @@ -422,14 +422,25 @@ def depth(self): self.barometer.depth.values.copy(), error=self.error.index) - if depth.min() < -230 and self.accelerometer.depth.min() > -230: - LOG.warning('Fused depth result produced a profile > 230 cm. Defaulting to accelerometer') - self._depth = self.accelerometer.depth - - elif depth.min() < -230 and self.barometer.depth.min() > -230: - LOG.warning('Fused and accelerometer depth resulted in a profile > 230 cm. Defaulting to barometer') - self._depth = self.barometer.depth - + # Failed fusion + unrealistic_depth = 230 + travel = abs(depth.max() - depth.min()) + + # Unrealistic depth + if travel > unrealistic_depth: + warn_msg = f'Fused depth result produced a profile > {unrealistic_depth} cm.' + + # Check if acceleration alone is reasonable + if self.accelerometer.distance_traveled < unrealistic_depth: + LOG.warning(warn_msg + ' Defaulting to accelerometer') + self._depth = self.accelerometer.depth + + # Check if barometer alone is reasonable + elif self.barometer.distance_traveled < unrealistic_depth: + LOG.warning(warn_msg + ' Defaulting to barometer') + self._depth = self.barometer.depth + else: + LOG.error(warn_msg + ' Both sensors unrealistic, using data as is.') else: self._depth = pd.Series(data=depth, index=self.raw['time']) @@ -442,7 +453,7 @@ def depth(self): self._depth = self.barometer.depth # Assign positions of each event detected - self.assign_event_depths() + self.assign_event_depths(self._depth) return self._depth @@ -479,6 +490,38 @@ def stop(self): return self._stop + def assign_surface_depths(self, depth:pd.Series): + # Event according the NIR sensors + idx = self.surface.nir.index + self._surface.nir.depth = depth.iloc[idx] + + # Event according to the force sensor + force_surface_depth = self._surface.nir.depth + self.surface_detection_offset + f_idx = abs(depth - force_surface_depth).argmin() + # Retrieve force estimated start + f_start = get_sensor_start(self.raw['Sensor1'], max_threshold=0.02, threshold=-0.02) + f_start = f_start or f_idx + + # If the force start is before the NIR start then adjust + if f_start < self.start.index: + LOG.info(f'Choosing motion start ({self.start.index}) over force start ({f_start})...') + f_idx = self.start.index + force_surface_depth = depth.iloc[f_idx] + + elif f_start < f_idx: + LOG.info(f'Choosing force start ({f_start}) over nir derived ({f_idx})...') + f_idx = f_start + force_surface_depth = depth.iloc[f_idx] + + self._surface.force.index = f_idx + self._surface.force.depth = depth.iloc[f_idx] + self._surface.force.time = self.raw['time'].iloc[f_idx] + + # Adjust surface detection to modify the start if there is conflict. + if self._surface.nir.time < self.start.time: + self._start = self._surface.nir + self._start.name = 'start' + @property def surface(self): """ @@ -490,35 +533,11 @@ def surface(self): if idx == 0: LOG.warning("Unable to find snow surface, defaulting to first data point") # Event according the NIR sensors - depth = self.depth.iloc[idx] - nir = Event(name='surface', index=idx, depth=depth, time=self.raw['time'].iloc[idx]) - - # Event according to the force sensor - force_surface_depth = depth + self.surface_detection_offset - f_idx = abs(self.depth - force_surface_depth).argmin() - # Retrieve force estimated start - f_start = get_sensor_start(self.raw['Sensor1'], max_threshold=0.02, threshold=-0.02) - f_start = f_start or f_idx - - # If the force start is before the NIR start then adjust - if f_start < self.start.index: - LOG.info(f'Choosing motion start ({self.start.index}) over force start ({f_start})...') - f_idx = self.start.index - force_surface_depth = self.depth.iloc[f_idx] - - elif f_start < f_idx: - LOG.info(f'Choosing force start ({f_start}) over nir derived ({f_idx})...') - f_idx = f_start - force_surface_depth = self.depth.iloc[f_idx] - - force = Event(name='surface', index=f_idx, depth=force_surface_depth, time=self.raw['time'].iloc[f_idx]) + nir = Event(name='surface', index=idx, depth=None, time=self.raw['time'].iloc[idx]) + # Force events placeholder + force = Event(name='surface', index=None, depth=None, time=None) self._surface = SimpleNamespace(name='surface', nir=nir, force=force) - # Allow surface detection to modify the start if there is conflict. - if nir.time < self.start.time: - self._start = nir - self._start.name = 'start' - return self._surface @property @@ -648,10 +667,7 @@ def fuse_depths(cls, acc_depth, baro_depth, error=None): # Scale total sensor_diff = abs(acc_bottom) - abs(baro_bottom) delta = 0.572 * abs(acc_bottom) + 0.308 * abs(baro_bottom) + 0.264 * sensor_diff + 8.916 - # delta = (acc_bottom * (5 - scale) + baro_bottom * scale) / 5 avg = (avg / avg_bottom) * -1 * delta - # from study_lyte.plotting import plot_ts - # ax = plot_ts(avg, show=True) return avg From 624a5f4bb86fcb16ed7379fb4075c0496536a689 Mon Sep 17 00:00:00 2001 From: micah johnson Date: Tue, 27 Jan 2026 22:04:44 -0700 Subject: [PATCH 2/2] Added back in original fused data in the event things fully blow up --- study_lyte/profile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/study_lyte/profile.py b/study_lyte/profile.py index dc04f52..2777b75 100644 --- a/study_lyte/profile.py +++ b/study_lyte/profile.py @@ -442,7 +442,8 @@ def depth(self): LOG.warning(warn_msg + ' Defaulting to barometer') self._depth = self.barometer.depth else: - LOG.error(warn_msg + ' Both sensors unrealistic, using data as is.') + LOG.error(warn_msg + ' Alternate sensors also unrealistic, using data as is.') + self._depth = pd.Series(data=depth, index=self.raw['time']) else: self._depth = pd.Series(data=depth, index=self.raw['time'])