Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b87592b
fix wraparound +/-180 longitudes
Dec 15, 2025
ef98053
new util func to concatenate spectral obj
Dec 16, 2025
4105ac7
new func to check if the longitudes are grouped around the 180 meridian
Dec 16, 2025
08bf238
fix expand_area to take into account the case of longitudes crossing …
Dec 16, 2025
0133d6e
ignore local test folder and dnora_bnd temp folder
Dec 16, 2025
d4fc863
fix ERA5 reader for the case where lon are grouped around meridian 180
Dec 16, 2025
4ad0887
fix point picker expand area and Area func for the meridian 180 case
Dec 16, 2025
3df2b93
propagate max calls parameter to func import_data
Dec 16, 2025
fe7d2de
import new utils func and propagate max calls
Dec 16, 2025
279acbf
if we cluster, display the area covered by each cluster.
Dec 16, 2025
607d441
adapt search grid for points grouped around meridian 180
Dec 16, 2025
f18d699
if clustering : loop over cluster, create a grid for each cluster > i…
Dec 16, 2025
afd486f
Merge branch 'MET-OM:main' into ImportSpecCluster
lenny-frno Dec 16, 2025
72249b3
added test for point picking wrapped cases
bjorkqvi Feb 11, 2026
4da9fb0
fixed area picker
bjorkqvi Feb 11, 2026
520598f
pick around interest points, not entire grid
bjorkqvi Feb 11, 2026
64b3b12
use Union for compatibility
bjorkqvi Feb 11, 2026
33f6c9e
updated dependecies
bjorkqvi Feb 11, 2026
f44ed47
updated concatenating
bjorkqvi Feb 11, 2026
4e7d083
fixed finding points in wrapped situations
bjorkqvi Feb 11, 2026
4ed8303
added function to calculate wrapped lon edges
bjorkqvi Feb 11, 2026
92e420f
removed unused feature from function
bjorkqvi Feb 11, 2026
6b91dfb
removed old concatenation function
bjorkqvi Feb 11, 2026
ad8c736
dependencies
bjorkqvi Feb 11, 2026
6da26a6
removed example
bjorkqvi Feb 11, 2026
773d9da
fixed toml syntax
bjorkqvi Feb 11, 2026
5453282
warning rom re fixed
bjorkqvi Feb 11, 2026
785f9be
worked around immutability in pandas
bjorkqvi Feb 11, 2026
f644fa8
Merge branch 'main' into pr-era5-cluster
bjorkqvi Feb 11, 2026
f883ef8
fixed indentation
bjorkqvi Feb 11, 2026
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ cython_debug/
#.idea/
.vscode/*
tests/.vscode/*
test/
dnora/.vscode/*
tests/dnora_wind_temp/*
tests/dnora_current_temp/*
Expand All @@ -177,3 +178,4 @@ grd/
dnplot/
spc/
wlv/
dnora_bnd_temp/
2 changes: 1 addition & 1 deletion dnora/file_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ def replace_objects(
"""
for obj_type, obj_name in dict_of_object_names.items():
if obj_name is not None:
filename = re.sub(f"#{obj_type.name}", obj_name, filename, 1)
filename = re.sub(f"#{obj_type.name}", obj_name, filename, count=1)

return filename

Expand Down
176 changes: 129 additions & 47 deletions dnora/modelrun/import_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
from dnora.pick import PointPicker, Area, NearestGridPoint
from geo_skeletons import PointSkeleton
from dnora import msg
from dnora.utils.distance import clustered_around_lon180, wrapped_lon_edges
from dnora.utils.grid import cluster_points

from dnora.grid import TriGrid
from dnora.grid.mask import All

def import_data(
grid: Grid,
Expand All @@ -28,58 +32,138 @@ def import_data(
filename: str,
point_mask=None,
point_picker=None,
max_calls: int = None,
**kwargs,
) -> DnoraObject:
"""Imports data using DataReader and creates and returns a DNORA object"""
msg.plain("")
msg.print_line(marker="+")
msg.plain(f"Starting import of data: {obj_type.name}")
msg.print_line(marker="+")
msg.plain(
f"Area: {grid.core.x_str}: {grid.edges('lon',native=True)}, {grid.core.y_str}: {grid.edges('lat',native=True)}"

if grid.core.x_str == 'lon' and clustered_around_lon180(grid.lon()):
lon0 = float(np.min(grid.lon()[grid.lon() > 0]))
lon1 = float(np.max(grid.lon()[grid.lon() < 0]))
msg.plain(
f"Area: {grid.core.x_str}: {(lon0, lon1)}, "
f"{grid.core.y_str}: {grid.edges('lat',native=True)}"
)
else:
msg.plain(
f"Area: {grid.core.x_str}: {grid.edges('lon',native=True)}, "
f"{grid.core.y_str}: {grid.edges('lat',native=True)}"
)

if max_calls is not None:
msg.plain(f"Max calls set to {max_calls}. Clustering will be used if needed.")
clustered_points = cluster_points(lon = grid.lon(), lat = grid.lat(), N_cluster = max_calls)
msg.plain(f"Number of clusters created: {len(clustered_points)}")
for i, cluster in enumerate(clustered_points):
if clustered_around_lon180(cluster[:,0]):
lon0 = float(np.min(cluster[cluster[:,0] > 0][:,0]))
lon1 = float(np.max(cluster[cluster[:,0] < 0][:,0]))
msg.plain(f"Area cluster {i+1} (wrapped): lon: {lon0:.2f} to {lon1:.2f}, lat: {cluster[:,1].min():.2f} to {cluster[:,1].max():.2f}")
else:
msg.plain(f"Area cluster {i+1}: lon: {cluster[:,0].min():.2f} to {cluster[:,0].max():.2f}, lat: {cluster[:,1].min():.2f} to {cluster[:,1].max():.2f}")

msg.plain(f"{start_time} - {end_time}")

if dry_run:
msg.info("Dry run! No data will be imported.")
return
if max_calls is not None:
if dnora_objects.get(obj_type).is_gridded():
raise ValueError("Cannot use 'max_calls' when importing gridded objects!")

if not dnora_objects.get(obj_type).is_gridded():
msg.header(point_picker, "Choosing points to import...")
inds = pick_points(
grid,
msg.plain("Point picking with clustering...")

obj_list = []
clustered_points = [clustered_points[-1]]
for i, cluster in enumerate(clustered_points):
msg.plain(f"\nPicking points for cluster {i+1}...")
lon,lat = cluster[:,0], cluster[:,1]

points = TriGrid(lon=lon[:], lat=lat[:], name=f"{grid.name}_cluster_{i}")
points.set_boundary_points(All())
inds = pick_points(
points,
reader,
start_time,
point_picker,
expansion_factor,
points.boundary_mask(),
source,
folder,
filename,
**kwargs,
)
if len(inds) < 1:
msg.warning("PointPicker didn't find any points. Aborting import of data.")
return

msg.plain(f"\nImporting data for cluster {i+1}...")
obj = read_data_and_create_object(
obj_type,
reader,
expansion_factor,
points,
start_time,
end_time,
name,
source,
folder,
filename,
inds,
**kwargs,
)
obj_list.append(obj)

msg.plain("Concatenating data from all clusters...")
obj = None
for ob in obj_list:
if obj is None:
obj = ob
else:
obj = obj.absorb(ob, dim='inds')
msg.plain("Concatenation done.")


else:
if not dnora_objects.get(obj_type).is_gridded():
msg.header(point_picker, "Choosing points to import...")
inds = pick_points(
grid,
reader,
start_time,
point_picker,
expansion_factor,
point_mask,
source,
folder,
filename,
**kwargs,
)
if len(inds) < 1:
msg.warning("PointPicker didn't find any points. Aborting import of data.")
return
else:
inds = np.array([])
msg.header(reader, f"Importing {obj_type.name}...")
obj = read_data_and_create_object(
obj_type,
reader,
start_time,
point_picker,
expansion_factor,
point_mask,
grid,
start_time,
end_time,
name,
source,
folder,
filename,
inds,
**kwargs,
)
if len(inds) < 1:
msg.warning("PointPicker didn't find any points. Aborting import of data.")
return
else:
inds = np.array([])

msg.header(reader, f"Importing {obj_type.name}...")

obj = read_data_and_create_object(
obj_type,
reader,
expansion_factor,
grid,
start_time,
end_time,
name,
source,
folder,
filename,
inds,
**kwargs,
)
return obj


Expand Down Expand Up @@ -193,31 +277,29 @@ def pick_points(
y=available_points.get("y"),
)

if not np.all(np.logical_not(point_mask)):
interest_points = PointSkeleton.from_skeleton(grid, mask=point_mask)
slat = interest_points.edges("lat")
slon = wrapped_lon_edges(interest_points)
else:
interest_points = None
slat = grid.edges("lat")
slon = wrapped_lon_edges(grid)
## Only take points that are reasonable close to the wanted grid
## This speeds up the searcg considerably, especially if we have points over 84 lat
## since then the fast cartesian searhc is not possible
## Set a limit for angles close to 90 and -90 lat and -180 and 180 longitude
slon, slat = grid.edges("lon"), grid.edges("lat")
eps = 1e-12
search_grid = Grid(lat=(max(slat[0] - 3, -90 + eps), min(slat[1] + 3, 90 - eps)),
lon=(max(slon[0] - 6, -180 + eps), min(slon[1] + 6, 180 - eps)))
if isinstance(point_picker, NearestGridPoint):
search_inds = Area()(search_grid, all_points, expansion_factor=1)
eps = 1e-12
slon = (slon[0] - 6, slon[1] + 6)

search_grid = Grid(lat=(max(slat[0] - 3, -90 + eps), min(slat[1] + 3, 90 - eps)),
lon=(max(slon[0] - 6, -180 + eps), min(slon[1] + 6, 180 - eps)))
search_inds = Area()(search_grid, all_points, expansion_factor=1, lon=slon)
else:
search_inds = all_points.inds()

# if np.all(np.logical_not(point_mask)):
# msg.warning(
# "None of the points set to interest points! Aborting import of data."
# )
# return
# else:
# interest_points = PointSkeleton.from_skeleton(grid, mask=point_mask)

if not np.all(np.logical_not(point_mask)):
interest_points = PointSkeleton.from_skeleton(grid, mask=point_mask)
else:
interest_points = None

inds = point_picker(
grid=grid,
Expand All @@ -227,7 +309,7 @@ def pick_points(
fast=True,
**kwargs,
)

if len(inds) < 1:
return np.array([])

return search_inds[inds]
14 changes: 12 additions & 2 deletions dnora/modelrun/modelrun.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from dnora.type_manager.model_formats import ModelFormat
from dnora.spectral_grid import SpectralGrid

import dnplot

from dnora import msg
from dnora.cacher.cache_decorator import cached_reader

Expand Down Expand Up @@ -159,7 +159,13 @@ def __init__(
self._nest = {}
self._parent = None

self.plot = dnplot.Matplotlib(self)
try:
import dnplot
self.plot = dnplot.Matplotlib(self)
except ImportError:
msg.info(
"dnplot is not installed. Please install dnplot to enable plotting functionality.")

self._dnora_objects: dict[DnoraDataType, DnoraObject] = {
DnoraDataType.GRID: grid,
}
Expand Down Expand Up @@ -295,6 +301,7 @@ def _import_data(
point_mask=None,
point_picker=None,
post_process: bool = True,
max_calls: Optional[int] = None,
**kwargs,
):
"""Performs import and returns DNORA object"""
Expand Down Expand Up @@ -370,6 +377,7 @@ def _import_data(
filename=filename_to_use,
point_picker=point_picker,
point_mask=point_mask,
max_calls=max_calls,
**kwargs,
)

Expand Down Expand Up @@ -478,6 +486,7 @@ def import_spectra(
source: Union[str, DataSource] = DataSource.UNDEFINED,
folder: Optional[str] = None,
filename: Optional[str] = None,
max_calls: Optional[int] = None,
**kwargs,
) -> None:
self._import_data(
Expand All @@ -491,6 +500,7 @@ def import_spectra(
filename,
point_mask=self.grid().boundary_mask(),
point_picker=point_picker,
max_calls=max_calls,
**kwargs,
)

Expand Down
25 changes: 20 additions & 5 deletions dnora/pick/point_pickers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from abc import ABC, abstractmethod
from typing import Union
from geo_skeletons import PointSkeleton

from dnora.utils.distance import clustered_around_lon180
from typing import TYPE_CHECKING

if TYPE_CHECKING:
Expand Down Expand Up @@ -93,11 +93,12 @@ def __call__(
grid: Union[Grid, TriGrid],
all_points: PointSkeleton,
expansion_factor: float = 1.5,
lon: tuple = None,
**kwargs,
) -> np.ndarray:

msg.process(f"Using expansion_factor = {expansion_factor:.2f}")

# Define area to search in
if grid.core.is_cartesian():
number, zone = grid.utm()
Expand All @@ -106,16 +107,30 @@ def __call__(
grid.edges("x"), grid.edges("y"), expansion_factor
)
x_all, y_all = all_points.xy()
maskx = np.logical_and(x_all >= x[0], x_all <= x[1])

else:
# If we want to pass a search grid that goes beyond -180 180
lat = grid.edges("lat")
lon = lon or grid.edges("lon")
x, y = utils.grid.expand_area(
grid.edges("lon"), grid.edges("lat"), expansion_factor
lon, lat, expansion_factor, cross_180=True
)
x_all, y_all = all_points.lonlat()

maskx = np.logical_and(x_all >= x[0], x_all <= x[1])
if x[0]>x[1]:
maskx = np.logical_or(x_all <= x[0], x_all >= x[1])
else:
maskx = np.logical_and(x_all >= x[0], x_all <= x[1])
if x[0] < -180:
maskx = np.logical_or(maskx, x_all > x[0] + 360)
if x[1] > 180:
maskx = np.logical_or(maskx, x_all < x[1] - 360)


masky = np.logical_and(y_all >= y[0], y_all <= y[1])
mask = np.logical_and(maskx, masky)

inds = np.where(mask)[0]

msg.info(
Expand Down
Loading
Loading