Source code for pelagos_py.steps.processing.bbp

# This file is part of pelagos_py.
#
# Copyright 2025-2026 National Oceanography Centre and The Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Pipeline steps for deriving particulate backscatter (BBP) from beta and isolating BBP spikes."""

#### Mandatory imports ####
from pelagos_py.steps.base_step import BaseStep, register_step
from pelagos_py.utils.qc_handling import QCHandlingMixin
from pelagos_py.utils.processing_utils import *
import pelagos_py.utils.diagnostics as diag

#### Custom imports ####
import xarray as xr
import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np
import glidertools as gt


@register_step
[docs] class BBPFromBeta(BaseStep, QCHandlingMixin): step_name = "BBP from Beta" required_variables = ["TIME", "DEPTH", "TEMP", "PRAC_SALINITY"] provided_variables = [] parameter_schema = { "apply_to": { "type": str, "default": "BBP700", "description": "Name of the variable to convert.", }, "output_as": { "type": str, "default": "BBP700", "description": "Name for the output variable added to the dataset.", }, "theta": { "type": float, "default": 124, "description": "Effective optical backscatter scattering angle (degrees).", }, "xfactor": { "type": float, "default": 1.076, "description": "Chi factor scaling particulate scattering to total backscatter.", }, }
[docs] def run(self): """ Example ------- :: - name: "BBP from Beta" parameters: apply_to: "BBP700" output_as: "BBP700" theta: 124 xfactor: 1.076 diagnostics: false Returns ------- """ self.filter_qc() # Get the required variables self.data_subset = self.data[ ["TIME", "PROFILE_NUMBER", "DEPTH", "TEMP", "PRAC_SALINITY", self.apply_to] ] # Interp DEPTH, TEMP and PRAC_SALINITY for var in ["DEPTH", "TEMP", "PRAC_SALINITY"]: self.data_subset[var][:] = interpolate_nans( self.data_subset[var], self.data_subset["TIME"] ) # Apply the correction bbp_corrected = gt.flo_functions.flo_bback_total( self.data_subset[self.apply_to], self.data_subset["TEMP"], self.data_subset["PRAC_SALINITY"], self.theta, 700, self.xfactor, ) # Stitch back into the data self.data[self.output_as] = bbp_corrected self.reconstruct_data() self.update_qc() # Generate QC if a new variable is added. Otherwise warn the user that input is being overwritten. if self.apply_to != self.output_as: self.generate_qc({f"{self.output_as}_QC": [f"{self.apply_to}_QC"]}) else: self.log_warn( f"'apply_to' and 'output_as' are the same. This will cause {self.apply_to} to be overwritten." ) if self.diagnostics: self.generate_diagnostics() self.context["data"] = self.data return self.context
def generate_diagnostics(self): mpl.use("tkagg") # Clean both datasets beta_clean = remove_outliers(self.data_subset[self.apply_to]) bbp_clean = remove_outliers(self.data[self.output_as]) # Plot plt.figure(figsize=(10, 6)) plt.boxplot( [beta_clean, bbp_clean], vert=True, patch_artist=True, labels=["Beta", "BBP"], ) plt.title("Beta vs BBP") plt.ylabel("Value") plt.grid(True, linestyle="--", alpha=0.6) plt.show(block=True)
@register_step
[docs] class IsolateBBPSpikes(BaseStep, QCHandlingMixin): step_name = "Isolate BBP Spikes" required_variables = ["TIME"] provided_variables = [] parameter_schema = { "apply_to": { "type": str, "default": "BBP700", "description": "Name of the variable to filter.", }, "window_size": { "type": int, "default": 50, "description": "Median/minmax filter window size in samples.", }, "method": { "type": str, "default": "median", "description": "Filter method used to determine the baseline.", }, }
[docs] def run(self): """ Example ------- :: - name: "Isolate BBP Spikes" parameters: apply_to: "BBP700" window_size: 50 method: "median" diagnostics: false Returns ------- """ self.filter_qc() self.baseline, self.spikes = gt.cleaning.despike( self.data[self.apply_to], self.window_size, spike_method=self.method ) self.data[f"{self.apply_to}_BASELINE"] = self.baseline self.data[f"{self.apply_to}_SPIKES"] = self.spikes self.reconstruct_data() self.update_qc() # Generate QC if a new variable is added. Otherwise warn the user that input is being overwritten. self.generate_qc( { f"{self.apply_to}_BASELINE_QC": [f"{self.apply_to}_QC"], f"{self.apply_to}_SPIKES_QC": [f"{self.apply_to}_QC"], } ) if self.diagnostics: self.generate_diagnostics() self.context["data"] = self.data return self.context
def generate_diagnostics(self): mpl.use("tkagg") raw = self.data[self.apply_to] # Plot fig, axs = plt.subplots( nrows=2, figsize=(10, 6), height_ratios=(2, 1), sharex=True ) # Plot original and baseline time series axs[0].plot( self.data["TIME"][~np.isnan(raw)], raw[~np.isnan(raw)], ls="--", c="gray", label="Raw", ) axs[0].plot( self.data["TIME"][~np.isnan(self.baseline)], self.baseline[~np.isnan(self.baseline)], c="b", alpha=0.5, label="Baseline", ) # Plot spike points axs[1].plot( self.data["TIME"][~np.isnan(self.spikes)], self.spikes[~np.isnan(self.spikes)], marker="o", c="r", label="Spikes", ) for ax in axs: ax.legend(loc="upper right") ax.set( xlabel="Time", ylabel=self.apply_to, title=f"{self.apply_to}: Baseline Timeseries & Spikes", ) fig.tight_layout() plt.show(block=True)