#!/usr/bin/env python3
"""
Created on Thu Aug 19 10:20:12 2025
@author: jbroust
"""
import functools
import os
import numpy as np
import xarray as xr
from .. import geo as sgeo
from .download import load_from_ds
[docs]
class Profile:
"""Individual in-situ profile with temperature and salinity data"""
[docs]
def __init__(self, prf):
# Extract scalar values for single-element arrays
time_vals = prf.TIME.values
self.time = time_vals[0] if time_vals.ndim > 0 and time_vals.size == 1 else time_vals
lat_vals = prf.LATITUDE.values
self.lat = float(lat_vals.flat[0]) if lat_vals.size >= 1 else lat_vals
lon_vals = prf.LONGITUDE.values
self.lon = float(lon_vals.flat[0]) if lon_vals.size >= 1 else lon_vals
self.depth = np.arange(1, 2001)
self.temp = np.interp(
self.depth,
prf.PRES_ADJUSTED,
prf.TEMP_ADJUSTED,
left=np.nan,
right=np.nan,
)
self.sal = np.interp(
self.depth,
prf.PRES_ADJUSTED,
prf.PSAL_ADJUSTED,
left=np.nan,
right=np.nan,
)
self.valid = 1.5 * np.sum(np.isnan(self.temp)) < len(self.temp)
[docs]
class Profiles:
"""Collection of in-situ profiles with temperature and salinity data
Manages a collection of Profile objects, providing methods to load from datasets,
convert to xarray format, save to NetCDF, and associate with eddies.
Parameters
----------
time : xarray.DataArray
Time coordinate for the profiles.
root_path : str
Root directory path for data storage.
brut_prf : xarray.Dataset
Raw profile dataset.
Attributes
----------
profiles : list of Profile
List of valid Profile objects.
years : ndarray
Array of years covered by the profiles.
ds : xarray.Dataset
Profiles converted to xarray Dataset format (cached property).
"""
[docs]
def __init__(self, time, root_path, brut_prf):
"""Initialize Profiles collection
Parameters
----------
time : xarray.DataArray
Time coordinate for the profiles.
root_path : str
Root directory path for data storage.
brut_prf : xarray.Dataset
Raw profile dataset.
"""
self.root_path = root_path
self.time = time
self.years = np.arange(self.time.min().dt.year.values, self.time.max().dt.year.values + 1)
self.profiles = []
for i in brut_prf.N_PROF:
prf = Profile(brut_prf.isel(N_PROF=i))
if prf.valid:
self.profiles.append(prf)
[docs]
@classmethod
def from_ds(cls, ds, root_path, max_depth=1000):
"""Create Profiles from an xarray Dataset
Parameters
----------
ds : xarray.Dataset
Dataset with time coordinate.
root_path : str
Root directory path for data storage.
Returns
-------
Profiles
New Profiles instance.
"""
brut_prf = load_from_ds(ds, root_path, max_depth=max_depth)
return cls(ds.time, root_path, brut_prf)
@functools.cached_property
def ds(self):
"""create profile array"""
lats = np.array([prf.lat for prf in self.profiles])
lons = np.array([prf.lon for prf in self.profiles])
times = np.array([prf.time for prf in self.profiles])
temp = np.array([prf.temp for prf in self.profiles])
sal = np.array([prf.sal for prf in self.profiles])
p_id = np.arange(0, len(lats), dtype="int32")
ds = xr.Dataset(
{
"time": (("profil"), times),
"p_id": (("profil"), p_id),
"lat": (("profil"), lats),
"lon": (("profil"), lons),
"temp": (("profil", "depth"), temp),
"sal": (("profil", "depth"), sal),
},
coords={
"profil": np.arange(0, len(lats)),
"depth": self.profiles[0].depth,
},
)
return ds
[docs]
def to_netcdf(self, name=None, path=None):
"""Save profiles to NetCDF format"""
if not path:
path = self.root_path
if not name:
name = f"profil_{self.years[0]}_{self.years[-1]}.nc"
self.ds.to_netcdf(
os.path.join(
path,
name,
)
)
[docs]
def associate(self, eddies, nlag=2):
"""Associate profiles with eddies
Attributes profiles to eddies (EvolEddies2D object) and adds info to profiles
if they are considered colocalized within a structure.
"""
eddy_pos = np.ones(len(self.ds.profil)) * -1
for eddy in eddies.eddies: # list of Eddies2D object object
tstart = eddy.time - np.timedelta64(nlag, "D")
tend = eddy.time + np.timedelta64(nlag, "D")
ind_prf = np.where((self.ds.time >= tstart) & (self.ds.time <= tend))[0]
prf = self.ds.isel(profil=ind_prf)
for e in eddy.eddies: # list of Raw2dEddies object
e.p_id = []
inside = e.contains_points(prf.lon, prf.lat)
for i, b in enumerate(inside):
if b:
e.p_id.append(prf.isel(profil=i).p_id.values)
eddy_pos[prf.isel(profil=i).p_id.values] = e.track_id
self.ds = self.ds.assign(eddy_pos=("profil", np.array(eddy_pos, dtype=np.int32)))
[docs]
def background(self, eddies, nlag, dlag):
"""Define background profile for each eddies
profiles are considered when they are in a window of nlag around the detection
date and at a maximum of dlag.
eddies is a EvolEddies2D object
nlag is a number of day
dlag is a distance expected in kilometers
"""
assert hasattr(self.ds, "eddy_pos"), (
"Please associate eddies with profile before computing background"
)
ds_bck = self.ds.where(self.ds.eddy_pos == -1, drop=True)
for eddy in eddies.eddies: # list of Eddies2D object object
tstart = eddy.time - np.timedelta64(nlag, "D")
tend = eddy.time + np.timedelta64(nlag, "D")
ind_prf = np.where((ds_bck.time >= tstart) & (ds_bck.time <= tend))[0]
prf_bck = ds_bck.isel(profil=ind_prf)
for e in eddy.eddies:
dlon = prf_bck.lon - e.lon
dlat = prf_bck.lat - e.lat
dx = sgeo.deg2m(dlon, e.lat)
dy = sgeo.deg2m(dlat)
dxy = np.sqrt(dx**2 + dy**2)
e_prf_bck = prf_bck.where(dxy < 1000 * dlag, drop=True)
e.bck_id = list(e_prf_bck.p_id.astype("int32").values)