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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ OceanArray implements a multi-stage processing pipeline for mooring data, contro
```bash
git clone https://github.com/ocean-uhh/oceanarray.git
cd oceanarray
python -m venv venv
source venv/bin/activate
pip install -r requirements-dev.txt
pip install -e .
```
Expand Down
6 changes: 4 additions & 2 deletions oceanarray/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def _print_report(basedir: str, mooring: str) -> None:
counts = f"{n_raw:>7} raw → {n_rec:>7} {stage_label}"
else:
counts = f"{'?':>7} raw → {n_rec:>7} {stage_label}"
except Exception:
except Exception: # noqa: BLE001 — intentional broad catch at I/O boundary
counts = f"{'HDF ERR':>7} raw → {n_rec:>7} {stage_label}"

mtime = datetime.datetime.fromtimestamp(
Expand All @@ -103,7 +103,7 @@ def _print_report(basedir: str, mooring: str) -> None:
print(
f" s/n {serial:<8} {counts} {t0} → {t1} depth: {depth} dt: {interval} processed: {mtime} vars: {', '.join(vars_present)}"
)
except Exception as e:
except Exception as e: # noqa: BLE001 — display helper; must not crash status output
print(f" {nc.name}: ERROR — {e}")


Expand Down Expand Up @@ -290,6 +290,7 @@ def cmd_validate(args: argparse.Namespace) -> int:


def build_parser() -> argparse.ArgumentParser:
"""Build and return the top-level argument parser for the oceanarray CLI."""
parser = argparse.ArgumentParser(
prog="oceanarray",
description="Oceanographic mooring data processing.",
Expand Down Expand Up @@ -511,6 +512,7 @@ def build_parser() -> argparse.ArgumentParser:


def main() -> None:
"""Entry point for the ``oceanarray`` command-line tool."""
parser = build_parser()
args = parser.parse_args()
sys.exit(args.func(args))
8 changes: 3 additions & 5 deletions oceanarray/clock_offset.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from oceanarray import find_deployment, tools


def load_mooring_instruments(mooring_name, base_dir, output_path, file_suffix="_raw"):
def load_mooring_instruments(mooring_name, base_dir, output_path, file_suffix="_raw"): # noqa: ARG001
"""Load all instruments for a mooring from netCDF files and enrich with YAML metadata.

Parameters
Expand Down Expand Up @@ -161,7 +161,7 @@ def interpolate_datasets_to_grid(datasets, time_grid):
else:
print(f" No time-dependent variables found in dataset {idx}")

except Exception as e:
except Exception as e: # noqa: BLE001 — log and continue; one bad dataset must not abort mooring
print(f" ERROR interpolating dataset {idx}: {e}")
continue

Expand Down Expand Up @@ -299,7 +299,7 @@ def calculate_timing_offsets(combined_ds, bin_width_sec=60):
# Find consensus group
vals = start_off0[np.isfinite(start_off0)]
if vals.size == 0:
raise RuntimeError("No finite start offsets to form consensus.")
raise RuntimeError("No finite start offsets to form consensus.") # noqa: TRY003

vmin, vmax = vals.min(), vals.max()
bins = np.arange(vmin - bin_width_sec, vmax + 2 * bin_width_sec, bin_width_sec)
Expand Down Expand Up @@ -362,7 +362,6 @@ def perform_lag_correlation_analysis(combined_ds, ref_index=0, sub_sample=5):
np.diff(combined_ds["time"].values) / np.timedelta64(1, "s")
)

n_full = len(combined_ds["temperature"][:, ref_index].values)
ref_temp_sub = combined_ds["temperature"][:, ref_index].values[::sub_sample]
n_sub = len(ref_temp_sub)

Expand Down Expand Up @@ -510,7 +509,6 @@ def print_correlation_summary(combined_ds, correlation_results):

"""
serial = combined_ds["serial_number"].values
depths = combined_ds["nominal_depth"].values

print("Lag Correlation Analysis Results:")
print("(Enter the summed value, no sign change, in the yaml as clock_offset)")
Expand Down
16 changes: 8 additions & 8 deletions oceanarray/find_deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,11 +387,11 @@ def find_deployment(
deployment_time=None,
recovery_time=None,
deployment_margin_hours=2.0,
surface_window_hours=24.0,
smooth_window=9,
method="sigma_band", # NEW: 'changepoint' or 'sigma_band'
band_sigma=3.0, # NEW: used when method='sigma_band'
dwell_seconds=1800,
surface_window_hours=24.0, # noqa: ARG001 — reserved for future surface-window logic
smooth_window=9, # noqa: ARG001 — reserved for future smoothing step
method="sigma_band", # noqa: ARG001 — reserved for future method dispatch
band_sigma=3.0, # noqa: ARG001 — reserved for future sigma_band method
dwell_seconds=1800, # noqa: ARG001 — reserved for future dwell filter
):
"""Populate ds with:
- start_time (N_LEVELS) : first deep time (when temps settle cold)
Expand All @@ -401,15 +401,15 @@ def find_deployment(
Returns ds with the variables added/updated.
"""
if var_name not in ds:
raise ValueError(f"{var_name!r} not found in dataset.")
raise ValueError(f"{var_name!r} not found in dataset.") # noqa: TRY003
if ds[var_name].dims != ("time", "N_LEVELS"):
raise ValueError(
raise ValueError( # noqa: TRY003
f"{var_name!r} must have dims ('time','N_LEVELS'). Got {ds[var_name].dims}"
)

nlev = ds.sizes.get("N_LEVELS", None)
if nlev is None:
raise ValueError("Dimension 'N_LEVELS' not found in dataset.")
raise ValueError("Dimension 'N_LEVELS' not found in dataset.") # noqa: TRY003

# ensure outputs exist with correct dtype/shape
if "start_time" not in ds:
Expand Down
20 changes: 11 additions & 9 deletions oceanarray/logger.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# oceanarray/logger.py
"""Logging configuration and helpers for the oceanarray processing pipeline."""

import datetime
import logging
from pathlib import Path
from typing import Any

# Global logger instance (will be configured by setup_logger)
log = logging.getLogger("oceanarray")
Expand All @@ -12,37 +14,37 @@
LOGGING_ENABLED = True


def enable_logging():
def enable_logging() -> None:
"""Enable logging globally."""
global LOGGING_ENABLED
LOGGING_ENABLED = True


def disable_logging():
def disable_logging() -> None:
"""Disable logging globally."""
global LOGGING_ENABLED
LOGGING_ENABLED = False


def log_info(message, *args):
def log_info(message: str, *args: Any) -> None:
"""Log an info message, if logging is enabled."""
if LOGGING_ENABLED:
log.info(message, *args, stacklevel=2)


def log_warning(message, *args):
def log_warning(message: str, *args: Any) -> None:
"""Log a warning message, if logging is enabled."""
if LOGGING_ENABLED:
log.warning(message, *args, stacklevel=2)


def log_error(message, *args):
def log_error(message: str, *args: Any) -> None:
"""Log an error message, if logging is enabled."""
if LOGGING_ENABLED:
log.error(message, *args, stacklevel=2)


def log_debug(message, *args):
def log_debug(message: str, *args: Any) -> None:
"""Log a debug message, if logging is enabled."""
if LOGGING_ENABLED:
log.debug(message, *args, stacklevel=2)
Expand Down Expand Up @@ -95,7 +97,7 @@ def setup_logger(array_name: str, output_dir: str = "logs") -> None:
log.info(f"Logger initialized for array: {array_name}, writing to {log_path}")


def load_logging_config():
def load_logging_config() -> dict:
"""Load the global logging configuration from config/logging.yaml.

Returns
Expand All @@ -116,7 +118,7 @@ def load_logging_config():
config_path = Path(__file__).parent / "config" / "logging.yaml"

if not config_path.exists():
raise FileNotFoundError(f"Logging config not found: {config_path}")
raise FileNotFoundError(f"Logging config not found: {config_path}") # noqa: TRY003

with open(config_path, "r") as f:
config = yaml.safe_load(f)
Expand Down
59 changes: 50 additions & 9 deletions oceanarray/mooring_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
"east_velocity",
"north_velocity",
"up_velocity",
"current_speed",
"current_direction",
"east_velocity_qc",
"north_velocity_qc",
"up_velocity_qc",
Expand Down Expand Up @@ -60,6 +62,8 @@
"east_velocity",
"north_velocity",
"up_velocity",
"current_speed",
"current_direction",
"velocity_beam1",
"velocity_beam2",
"velocity_beam3",
Expand Down Expand Up @@ -101,8 +105,18 @@ def _dms_to_deg(s: str) -> float:
return val


def _parse_latlon(cfg: dict):
def _parse_latlon(cfg: dict) -> tuple[float, float]:
"""Return (lat, lon) in decimal degrees from a mooring config dict."""
lat, lon, _ = _parse_latlon_with_source(cfg)
return lat, lon


def _parse_latlon_with_source(cfg: dict) -> tuple[float, float, str]:
"""Return (lat, lon, source_key) from a mooring config dict.

source_key is the YAML key pair used, e.g. 'seabed_latitude/seabed_longitude',
or 'unknown (defaulting to 0, 0)' if none found.
"""
for lat_key, lon_key in [
("seabed_latitude", "seabed_longitude"),
("deployment_latitude", "deployment_longitude"),
Expand All @@ -112,8 +126,12 @@ def _parse_latlon(cfg: dict):
lat_s = cfg.get(lat_key)
lon_s = cfg.get(lon_key)
if lat_s is not None and lon_s is not None:
return _dms_to_deg(str(lat_s)), _dms_to_deg(str(lon_s))
return 0.0, 0.0
return (
_dms_to_deg(str(lat_s)),
_dms_to_deg(str(lon_s)),
f"{lat_key}/{lon_key}",
)
return 0.0, 0.0, "unknown (defaulting to 0, 0)"


_SIGMA_META = {
Expand Down Expand Up @@ -291,7 +309,7 @@ def _linear_interp(
class MooringStacker:
"""Step 1: stack all instruments onto a common time axis → ``_stack.nc``."""

def __init__(self, base_dir: str):
def __init__(self, base_dir: str) -> None:
self.base_dir = Path(base_dir)

def stack(
Expand All @@ -300,6 +318,15 @@ def stack(
dt_seconds: int = 60,
force: bool = False,
) -> bool:
"""Stack all processed instruments for *mooring_name* onto a common time grid.

Reads ``_stage3.nc`` (falling back to ``_stage2.nc``) for every instrument
listed in the mooring YAML, resamples each to *dt_seconds* resolution, and
writes ``{mooring}_stack.nc`` under the mooring proc directory.

Returns True on success, False if no instruments could be loaded or an
unrecoverable error occurs.
"""
proc_dir = _get_proc_dir(self.base_dir, mooring_name)
try:
proc_dir_exists = proc_dir.exists()
Expand Down Expand Up @@ -399,7 +426,7 @@ def stack(
try:
ds = xr.open_dataset(info["nc_path"], decode_timedelta=False).load()
ds.close()
except Exception as e:
except Exception as e: # noqa: BLE001 — one bad instrument must not abort the stack
print(f" WARNING: Could not load {info['nc_path'].name}: {e}")
# Ensure scalar_meta lists stay length-consistent
for lst in scalar_meta.values():
Expand Down Expand Up @@ -513,7 +540,7 @@ def stack(
"reference_pressure_dbar": ref_p,
},
)
except Exception as exc:
except Exception as exc: # noqa: BLE001 — gsw failure must not abort stack
print(f" WARNING: could not compute potential density: {exc}")

# velocity_flag: element-wise worst QC flag across east/north/up velocity.
Expand Down Expand Up @@ -624,7 +651,7 @@ def stack(
"long_name": "Serial number of the pressure reference instrument used for tilt"
},
)
except Exception as exc:
except Exception as exc: # noqa: BLE001 — optional derived var; must not abort stack
print(f" WARNING: could not compute tilt_from_pressure: {exc}")

# Coordinate names — exclude these from scalar metadata to avoid name conflicts
Expand Down Expand Up @@ -712,7 +739,7 @@ def stack(
class MooringGridder:
"""Step 2: vertically interpolate stacked instruments onto a pressure grid → ``_grid.nc``."""

def __init__(self, base_dir: str):
def __init__(self, base_dir: str) -> None:
self.base_dir = Path(base_dir)

def grid(
Expand All @@ -723,6 +750,14 @@ def grid(
dp: float = 20.0,
force: bool = False,
) -> bool:
"""Interpolate the stacked mooring dataset onto a regular pressure grid.

Reads ``{mooring}_stack.nc``, interpolates all variables onto a pressure
axis from *p_start* to *p_end* in steps of *dp* dbar, and writes
``{mooring}_grid.nc``.

Returns True on success, False on error.
"""
proc_dir = _get_proc_dir(self.base_dir, mooring_name)
merge_path = proc_dir / f"{mooring_name}_stack.nc"
output_path = proc_dir / f"{mooring_name}_grid.nc"
Expand Down Expand Up @@ -821,7 +856,13 @@ def grid(
if _v in _GRIDDER_TSQC:
var_data[_v][~np.isfinite(var_data[_v])] = np.nan

_vel_vars = {"east_velocity", "north_velocity", "up_velocity"}
_vel_vars = {
"east_velocity",
"north_velocity",
"up_velocity",
"current_speed",
"current_direction",
}
if "velocity_flag" in ds.data_vars:
_vflag = ds["velocity_flag"].values.astype(np.float64)
_bad_vel = np.isin(np.round(_vflag).astype(np.int8), [3, 4, 9])
Expand Down
Loading
Loading