Source code for mhm_tools.common.resolution_handler
"""Module to handle resolution information for MHM tools."""
import logging
from pathlib import Path
import numpy as np
from mhm_tools.common.file_handler import get_coord_values, get_xarray_ds_from_file
from mhm_tools.common.logger import ErrorLogger
logger = logging.getLogger(__name__)
[docs]
class Resolution:
"""Class to hold resolution information."""
def __init__(
self,
l1=None,
l11=None,
l2=None,
l2_file=None,
l0=None,
l0_resolution=None,
l1_resolution=None,
l11_resolution=None,
l2_resolution=None,
raise_on_missmatch=True,
):
"""Initialize the Resolution class."""
self.l0 = l0 if l0 is not None else l0_resolution
self.l1 = l1 if l1 is not None else l1_resolution
self.l11 = l11 if l11 is not None else l11_resolution
self.l2 = l2 if l2 is not None else l2_resolution
self.l0 = float(self.l0) if self.l0 is not None else None
self.l1 = float(self.l1) if self.l1 is not None else None
self.l11 = float(self.l11) if self.l11 is not None else None
self.l2 = float(self.l2) if self.l2 is not None else None
self.l2_file = l2_file
if self.l2_file is not None:
self.l2_file = Path(self.l2_file)
if self.l2_file.is_dir():
# get the first netcdf file in the directory
nc_files = list(self.l2_file.rglob("*.nc"))
if len(nc_files) == 0:
with ErrorLogger(logger):
msg = f"No netcdf files found in {self.l2_file}."
raise FileNotFoundError(msg)
self.l2_file = nc_files[0]
elif not self.l2_file.is_file():
with ErrorLogger(logger):
msg = f"L2 file {self.l2_file} not found."
raise FileNotFoundError(msg)
if self.l2_file.suffix == ".nc":
with get_xarray_ds_from_file(self.l2_file) as ds:
lon = get_coord_values(ds, lon=True)
file_res = round(abs(lon[1] - lon[0]), 9)
if self.l2 is not None and abs(file_res - self.l2) > 1e-6:
msg = f"Provided l2_resolution {self.l2} differs from resolution derived from file {file_res}. Either provide the correct l2_resolution or remove it to use the resolution derived from the file."
if raise_on_missmatch:
with ErrorLogger(logger):
raise ValueError(msg)
logger.warning(msg)
self.l2 = file_res
elif self.l2 is None:
logger.info(
f"Derived l2_resolution {file_res} from {self.l2_file}"
)
self.l2 = file_res
else:
logger.error(
f"Unsupported file format for l2_file: {self.l2_file.suffix}"
)
self.l2_file = None
self.l11 = self.l11 if self.l11 is not None else self.l1
self.l2 = self.l2 if self.l2 is not None else self.l1
@property
def l0_resolution(self):
"""Backward-compatible alias for l0."""
return self.l0
@l0_resolution.setter
def l0_resolution(self, value):
self.l0 = value
@property
def l1_resolution(self):
"""Backward-compatible alias for l1."""
return self.l1
@l1_resolution.setter
def l1_resolution(self, value):
self.l1 = value
@property
def l11_resolution(self):
"""Backward-compatible alias for l11."""
return self.l11
@l11_resolution.setter
def l11_resolution(self, value):
self.l11 = value
@property
def l2_resolution(self):
"""Backward-compatible alias for l2."""
return self.l2
@l2_resolution.setter
def l2_resolution(self, value):
self.l2 = value
[docs]
def get_max_resolution(self):
"""Get the maximum resolution."""
return max(
r
for r in [
self.l1,
self.l11,
self.l2,
]
if r is not None
)
[docs]
def only_one_resolution(self):
"""Return the resolution if there is only one otherwise None."""
n = 0
res = None
for r in [
self.l0,
self.l1,
self.l11,
self.l2,
]:
if r is not None and r != res:
n += 1
res = r
if n == 1:
return res
return None
[docs]
def get_file_res(lon=None, lat=None, resolutions=None):
"""Get resolution from coordinates and match it to provided resolutions if close enough."""
if resolutions is None:
resolutions = Resolution()
if lon is not None and len(lon) > 1:
file_res = np.diff(lon.data).mean()
elif lat is not None and len(lat) > 1:
file_res = np.diff(lat.data).mean()
elif resolutions.only_one_resolution() is not None:
file_res = resolutions.only_one_resolution()
logger.warning(
"Taken resolution from provided resolutions as ds was to small to derive it."
)
else:
with ErrorLogger(logger):
error_msg = "Cannot determine file resolution: no valid lon or lat coordinates provided (need len > 1)."
raise ValueError(error_msg)
# if file_res is close to a resolution in resolutions, use that one to avoid floating point issues
if resolutions.l0 is not None and abs(file_res - resolutions.l0) < 1e-5:
logger.debug(
f"File resolution {file_res} is close to l0 resolution {resolutions.l0}. Using l0 resolution."
)
return resolutions.l0
if resolutions.l2 is not None and abs(file_res - resolutions.l2) < 1e-5:
logger.debug(
f"File resolution {file_res} is close to l2 resolution {resolutions.l2}. Using l2 resolution."
)
return resolutions.l2
if resolutions.l1 is not None and abs(file_res - resolutions.l1) < 1e-5:
logger.debug(
f"File resolution {file_res} is close to l1 resolution {resolutions.l1}. Using l1 resolution."
)
return resolutions.l1
if resolutions.l11 is not None and abs(file_res - resolutions.l11) < 1e-5:
logger.debug(
f"File resolution {file_res} is close to l11 resolution {resolutions.l11}. Using l11 resolution."
)
return resolutions.l11
logger.debug(
f"File resolution {file_res} does not match any provided resolution ({resolutions.l0}, {resolutions.l1}, {resolutions.l2}, {resolutions.l11})."
)
return file_res