@@ -124,22 +124,25 @@ def read_idf(path: str) -> Tuple[Dict[str, Any], np.ndarray]:
124
124
# equidistant IDFs
125
125
ieq = not struct .unpack ("?" , f .read (1 ))[0 ]
126
126
itb = struct .unpack ("?" , f .read (1 ))[0 ]
127
- if not ieq :
128
- raise ValueError (f"Non-equidistant IDF are not supported: { path } \n " )
129
127
130
128
f .read (2 ) # not used
131
129
if doubleprecision :
132
130
f .read (4 ) # not used
133
131
134
132
# dx and dy are stored positively in the IDF
135
133
# dy is made negative here to be consistent with the nonequidistant case
136
- attrs ["dx" ] = struct .unpack (floatformat , f .read (floatsize ))[0 ]
137
- attrs ["dy" ] = - struct .unpack (floatformat , f .read (floatsize ))[0 ]
134
+ if ieq :
135
+ attrs ["dx" ] = struct .unpack (floatformat , f .read (floatsize ))[0 ]
136
+ attrs ["dy" ] = - struct .unpack (floatformat , f .read (floatsize ))[0 ]
138
137
139
138
if itb :
140
139
attrs ["top" ] = struct .unpack (floatformat , f .read (floatsize ))[0 ]
141
140
attrs ["bot" ] = struct .unpack (floatformat , f .read (floatsize ))[0 ]
142
141
142
+ if not ieq :
143
+ attrs ["dx" ] = np .fromfile (f , dtype , ncol )
144
+ attrs ["dy" ] = - np .fromfile (f , dtype , nrow )
145
+
143
146
# These are derived, remove after using them downstream
144
147
attrs ["ncol" ] = ncol
145
148
attrs ["nrow" ] = nrow
@@ -225,7 +228,124 @@ def write(path, a, spatial_reference, nodata=1.0e20, dtype=np.float32):
225
228
return
226
229
227
230
228
- def convert_idf_to_gdal (path : str , crs_wkt : str ) -> Path :
231
+ def _check_for_equidistance (dx : np .ndarray ) -> Tuple [bool , float , float ]:
232
+ # Check if equidistant, even if the ieq flag says otherwise:
233
+ # Allow 0.1% deviation in cell size.
234
+ absdx = abs (dx )
235
+ dxmax = absdx .max ()
236
+ dxmin = absdx .min ()
237
+ if not np .allclose (dxmin , absdx , atol = abs (1.0e-4 * dxmin )):
238
+ return False , dxmin , dxmax
239
+ return True , dxmin , dxmax
240
+
241
+
242
+ def _resample (
243
+ attrs : Dict [str , Any ], values : np .ndarray , new_dx : float , new_dy : float
244
+ ) -> Tuple [Dict [str , Any ], np .ndarray ]:
245
+ """
246
+ Resample a non-equidistant grid to equidistant form.
247
+
248
+ We keep the lower-left (xmin, ymin) corner and work from there.
249
+
250
+ Parameters
251
+ ----------
252
+ attrs: dict
253
+ Contains IDF header information.
254
+ values: np.ndarray
255
+ Data values of the non-equidistant grid.
256
+ new_dx: float
257
+ New (equidistant) cell size along x.
258
+ new_dy: float
259
+ New (equidistant) cell size along y.
260
+
261
+ Returns
262
+ -------
263
+ resampled_attrs: dict
264
+ resampled_values: np.ndarray
265
+ """
266
+
267
+ def _inbounds_searchsorted (a : np .ndarray , v : np .ndarray ):
268
+ # np.searchsorted may give indexes larger than what array `a` allows.
269
+ return np .clip (np .searchsorted (a , v ), a_min = 0 , a_max = a .size - 1 )
270
+
271
+ xmin = attrs ["xmin" ]
272
+ ymin = attrs ["ymin" ]
273
+ # Make all cell sizes positive for simplicity.
274
+ dx = abs (attrs ["dx" ])
275
+ dy = abs (attrs ["dy" ])
276
+ new_dx = abs (new_dx )
277
+ new_dy = abs (new_dy )
278
+ # Note: iMOD stores dy from low to high!
279
+
280
+ # Compute the end location of each old cell
281
+ # Note: y is INCREASING here since searchsorted requires sorted numbers!
282
+ xend = xmin + dx .cumsum ()
283
+ yend = ymin + dy .cumsum ()
284
+ midx = np .arange (xmin , attrs ["xmax" ], new_dx ) + 0.5 * new_dx
285
+ midy = np .arange (ymin , attrs ["ymax" ], new_dy ) + 0.5 * new_dy
286
+ new_nrow = midy .size
287
+ new_ncol = midx .size
288
+
289
+ # Compute how to index into the old array.
290
+ # Search before which end the new midpoint lies.
291
+ column = _inbounds_searchsorted (xend , midx )
292
+ row = _inbounds_searchsorted (yend , midy )
293
+ iy , ix = (a .ravel () for a in np .meshgrid (row , column , indexing = "ij" ))
294
+
295
+ # Take into account that y is actually DECREASING in spatial rasters:
296
+ # index 0 becomes nrow - 1.
297
+ # index nrow - 1 becomes 0.
298
+ row = (attrs ["nrow" ] - 1 ) - row
299
+ resampled_values = values [iy , ix ].reshape ((new_nrow , new_ncol ))
300
+
301
+ # Updated attrs
302
+ resampled_attrs = {
303
+ "dx" : new_dx ,
304
+ "dy" : - new_dy ,
305
+ "ncol" : new_ncol ,
306
+ "nrow" : new_nrow ,
307
+ "xmin" : xmin ,
308
+ "xmax" : midx [- 1 ] + 0.5 * new_dx ,
309
+ "ymin" : ymin ,
310
+ "ymax" : midy [- 1 ] + 0.5 * new_dy ,
311
+ "nodata" : attrs ["nodata" ],
312
+ }
313
+ return resampled_attrs , resampled_values
314
+
315
+
316
+ def _maybe_resample (path : str , resample : bool ) -> Tuple [Dict [str , Any ], np .ndarray ]:
317
+ attrs , values = read_idf (path )
318
+
319
+ dx = attrs ["dx" ]
320
+ dy = attrs ["dy" ]
321
+ if isinstance (dx , float ) and isinstance (dy , float ):
322
+ # Exactly equidistant, nothing to do.
323
+ return attrs , values
324
+
325
+ # Check if approximately equidistant: allow 0.1% deviation)
326
+ dx = np .atleast_1d (attrs ["dx" ])
327
+ dy = np .atleast_1d (attrs ["dy" ])
328
+ fixed_dx , dxmin , dxmax = _check_for_equidistance (dx )
329
+ fixed_dy , dymin , dymax = _check_for_equidistance (dy )
330
+
331
+ if fixed_dx and fixed_dy :
332
+ # Approximately equidistant. Just modify dx and dy.
333
+ attrs ["dx" ] = dxmin
334
+ attrs ["dy" ] = - dymin
335
+ return attrs , values
336
+ else :
337
+ # Resample if allowed.
338
+ if resample :
339
+ return _resample (attrs , values , dxmin , dymin )
340
+ else :
341
+ raise ValueError (
342
+ f"IDF file is not equidistant along x or y.\n "
343
+ f"Cell sizes in x vary from { dxmin } to { dxmax } .\n "
344
+ f"Cell sizes in y vary from { dymin } to { dymax } .\n "
345
+ )
346
+
347
+
348
+ def convert_idf_to_gdal (path : str , crs_wkt : str , resample : bool ) -> Path :
229
349
"""
230
350
Read the contents of an iMOD IDF file and write it to a GeoTIFF, next to
231
351
the IDF. This is similar to how iMOD treats ASCII files.
@@ -237,13 +357,15 @@ def convert_idf_to_gdal(path: str, crs_wkt: str) -> Path:
237
357
crs_wkt: str
238
358
Desired CRS to write in the created GeoTIFF. IDFs do not have a CRS on
239
359
their own, one must be provided.
360
+ resample: bool
361
+ Whether to allow nearest-neigbhor resampling for non-equidistant grids.
240
362
241
363
Returns
242
364
-------
243
365
tiff_path: pathlib.Path
244
366
Path to the newly created GeoTIFF file.
245
367
"""
246
- attrs , values = read_idf (path )
368
+ attrs , values = _maybe_resample (path , resample )
247
369
248
370
path = Path (path )
249
371
tiff_path = (path .parent / (path .stem )).with_suffix (".tif" )
0 commit comments