Skip to content

Commit d25c3f1

Browse files
committed
wip: om domains
1 parent 835a926 commit d25c3f1

File tree

2 files changed

+480
-0
lines changed

2 files changed

+480
-0
lines changed

python/omfiles/om_domains.py

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
from abc import ABC, abstractmethod
2+
from functools import cached_property
3+
from typing import Optional, Tuple
4+
5+
import numpy as np
6+
7+
from omfiles.utils import _modulo_positive
8+
9+
10+
class AbstractGrid(ABC):
11+
"""
12+
Abstract base class for weather model grid definitions.
13+
14+
This defines the interface that all grid implementations must follow.
15+
"""
16+
17+
@property
18+
@abstractmethod
19+
def grid_type(self) -> str:
20+
"""Return the grid type identifier."""
21+
pass
22+
23+
@cached_property
24+
@abstractmethod
25+
def latitude(self) -> np.ndarray:
26+
"""
27+
Return the latitude coordinates array.
28+
29+
Uses cached_property to ensure the array is computed only once.
30+
"""
31+
pass
32+
33+
@cached_property
34+
@abstractmethod
35+
def longitude(self) -> np.ndarray:
36+
"""
37+
Return the longitude coordinates array.
38+
39+
Uses cached_property to ensure the array is computed only once.
40+
"""
41+
pass
42+
43+
@property
44+
@abstractmethod
45+
def shape(self) -> Tuple[int, int]:
46+
"""Return the grid shape as (n_lat, n_lon)."""
47+
pass
48+
49+
class RegularLatLonGrid(AbstractGrid):
50+
"""
51+
Regular latitude-longitude grid implementation.
52+
53+
This represents a standard equirectangular grid with uniform spacing.
54+
"""
55+
56+
def __init__(
57+
self,
58+
lat_start: float,
59+
lat_steps: int,
60+
lat_step_size: float,
61+
lon_start: float,
62+
lon_steps: int,
63+
lon_step_size: float,
64+
):
65+
"""
66+
Initialize a regular lat/lon grid.
67+
68+
Parameters:
69+
-----------
70+
lat_start : float
71+
Starting latitude value
72+
lat_steps : int
73+
Number of latitude points
74+
lat_step_size : float
75+
Spacing between latitude points
76+
lon_start : float
77+
Starting longitude value
78+
lon_steps : int
79+
Number of longitude points
80+
lon_step_size : float
81+
Spacing between longitude points
82+
"""
83+
self._lat_start = lat_start
84+
self._lat_steps = lat_steps
85+
self._lat_step_size = lat_step_size
86+
self._lon_start = lon_start
87+
self._lon_steps = lon_steps
88+
self._lon_step_size = lon_step_size
89+
90+
@property
91+
def grid_type(self) -> str:
92+
return "regular_latlon"
93+
94+
@cached_property
95+
def latitude(self) -> np.ndarray:
96+
"""
97+
Lazily compute and cache the latitude coordinate array.
98+
"""
99+
return np.linspace(
100+
self._lat_start,
101+
self._lat_start + self._lat_step_size * self._lat_steps,
102+
self._lat_steps,
103+
endpoint=False
104+
)
105+
106+
@cached_property
107+
def longitude(self) -> np.ndarray:
108+
"""
109+
Lazily compute and cache the longitude coordinate array.
110+
"""
111+
return np.linspace(
112+
self._lon_start,
113+
self._lon_start + self._lon_step_size * self._lon_steps,
114+
self._lon_steps,
115+
endpoint=False
116+
)
117+
118+
@property
119+
def shape(self) -> Tuple[int, int]:
120+
return (self._lat_steps, self._lon_steps)
121+
122+
def findPointXy(self, lat: float, lon: float) -> Optional[Tuple[int, int]]:
123+
"""
124+
Find grid point indices (x, y) for given lat/lon coordinates.
125+
126+
Parameters:
127+
-----------
128+
lat : float
129+
Latitude in degrees
130+
lon : float
131+
Longitude in degrees
132+
133+
Returns:
134+
--------
135+
tuple or None
136+
(x, y) grid indices if point is in grid, None otherwise
137+
"""
138+
# Calculate raw x and y indices
139+
x = int(round((lon - self._lon_start) / self._lon_step_size))
140+
y = int(round((lat - self._lat_start) / self._lat_step_size))
141+
142+
# Handle wrapping for global grids
143+
xx = _modulo_positive(x, self._lon_steps) if (self._lon_steps * self._lon_step_size) >= 359 else x
144+
yy = _modulo_positive(y, self._lat_steps) if (self._lat_steps * self._lat_step_size) >= 179 else y
145+
146+
# Check if point is within grid bounds
147+
if yy < 0 or xx < 0 or yy >= self._lat_steps or xx >= self._lon_steps:
148+
return None
149+
150+
return (xx, yy)
151+
152+
def getCoordinates(self, x: int, y: int) -> Tuple[float, float]:
153+
"""
154+
Get lat/lon coordinates for a given grid point indices.
155+
156+
Parameters:
157+
-----------
158+
x : longitude index
159+
y : latitude index
160+
161+
Returns:
162+
--------
163+
tuple
164+
(latitude, longitude) coordinates
165+
"""
166+
167+
lat = self._lat_start + float(y) * self._lat_step_size
168+
lon = self._lon_start + float(x) * self._lon_step_size
169+
170+
return (lat, lon)
171+
172+
class OmDomain:
173+
"""
174+
Class representing a domain configuration for a weather model.
175+
176+
This class provides metadata and configuration for different
177+
weather model grids used in Open-Meteo.
178+
"""
179+
180+
def __init__(
181+
self,
182+
name: str,
183+
grid: AbstractGrid,
184+
file_length: int,
185+
temporal_resolution_seconds: int = 3600
186+
):
187+
"""
188+
Initialize a domain configuration.
189+
190+
Parameters:
191+
-----------
192+
name : str
193+
Name of the domain
194+
grid : AbstractGrid
195+
Grid implementation for this domain
196+
file_length : int
197+
Number of time steps in each file chunk
198+
description : str, optional
199+
Human-readable description of the domain
200+
variables : list, optional
201+
List of variable names available in this domain
202+
temporal_resolution_seconds : int, optional
203+
Time resolution in seconds (default: 3600 = 1 hour)
204+
"""
205+
self.name = name
206+
self.grid = grid
207+
self.file_length = file_length
208+
self.temporal_resolution_seconds = temporal_resolution_seconds
209+
210+
def time_to_chunk_index(self, timestamp: np.datetime64) -> int:
211+
"""
212+
Convert a timestamp to a chunk index.
213+
214+
Parameters:
215+
-----------
216+
timestamp : np.datetime64
217+
The timestamp to convert
218+
219+
Returns:
220+
--------
221+
int
222+
The chunk index containing the timestamp
223+
"""
224+
# Basic implementation - can be extended with domain-specific logic
225+
epoch = np.datetime64('1970-01-01T00:00:00')
226+
seconds_since_epoch = (timestamp - epoch) / np.timedelta64(1, 's')
227+
228+
# Get temporal resolution from metadata (placeholder - real implementation would get from metadata)
229+
temporal_resolution = 3600 # 1 hour in seconds
230+
231+
# Calculate chunk index
232+
chunk_index = int(seconds_since_epoch / (self.file_length * temporal_resolution))
233+
return chunk_index
234+
235+
def get_chunk_time_range(self, chunk_index: int, temporal_resolution_seconds: int = 3600) -> np.ndarray:
236+
"""
237+
Get the time range covered by a specific chunk.
238+
239+
Parameters:
240+
-----------
241+
chunk_index : int
242+
Index of the chunk
243+
temporal_resolution_seconds : int, optional
244+
Time resolution in seconds (default: 3600 = 1 hour)
245+
246+
Returns:
247+
--------
248+
np.ndarray
249+
Array of datetime64 objects representing the time points in the chunk
250+
"""
251+
epoch = np.datetime64('1970-01-01T00:00:00')
252+
chunk_start_seconds = chunk_index * self.file_length * temporal_resolution_seconds
253+
start_time = epoch + np.timedelta64(chunk_start_seconds, 's')
254+
255+
# Create array of time points
256+
time_points = start_time + np.arange(self.file_length) * np.timedelta64(temporal_resolution_seconds, 's')
257+
return time_points
258+
259+
# - MARK: Create grid instances for supported domains
260+
261+
# DWD ICON D2 is regularized during download to nx: 1215, ny: 746 points
262+
# https://github.com/open-meteo/open-meteo/blob/1753ebb4966d05f61b17dd5bdf59700788d4a913/Sources/App/Icon/Icon.swift#L154
263+
_dwd_icon_d2_grid = RegularLatLonGrid(
264+
lat_start=43.18,
265+
lat_steps=746,
266+
lat_step_size=0.02,
267+
lon_start=-3.94,
268+
lon_steps=1215,
269+
lon_step_size=0.02
270+
)
271+
272+
# ECMWF IQFS grid is a regular global lat/lon grid, nx: 1440, ny: 721 points
273+
# https://github.com/open-meteo/open-meteo/blob/1753ebb4966d05f61b17dd5bdf59700788d4a913/Sources/App/Ecmwf/EcmwfDomain.swift#L107
274+
_ecmwf_ifs025_grid = RegularLatLonGrid(
275+
lat_start=-90,
276+
lat_steps=721,
277+
lat_step_size=360/1440,
278+
lon_start=-180,
279+
lon_steps=1440,
280+
lon_step_size=180/(721-1)
281+
)
282+
283+
DOMAINS: dict[str, OmDomain] = {
284+
'dwd_icon_d2': OmDomain(
285+
name='dwd_icon_d2',
286+
grid=_dwd_icon_d2_grid,
287+
file_length=121,
288+
temporal_resolution_seconds=3600 # 1 hour
289+
),
290+
'ecmwf_ifs025': OmDomain(
291+
name='ecmwf_ifs025',
292+
grid=_ecmwf_ifs025_grid,
293+
file_length=104,
294+
temporal_resolution_seconds=3600 # 1 hour
295+
),
296+
# Additional domains can be added here
297+
}

0 commit comments

Comments
 (0)