• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geo-engine / geoengine / 7006568925

27 Nov 2023 02:07PM UTC coverage: 89.651% (+0.2%) from 89.498%
7006568925

push

github

web-flow
Merge pull request #888 from geo-engine/raster_stacks

raster stacking

4032 of 4274 new or added lines in 107 files covered. (94.34%)

12 existing lines in 8 files now uncovered.

113020 of 126066 relevant lines covered (89.65%)

59901.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.54
/operators/src/processing/rasterization/mod.rs
1
use crate::engine::TypedVectorQueryProcessor::MultiPoint;
2
use crate::engine::{
3
    CanonicOperatorName, ExecutionContext, InitializedRasterOperator, InitializedSources,
4
    InitializedVectorOperator, Operator, OperatorName, QueryContext, QueryProcessor,
5
    RasterBandDescriptors, RasterOperator, RasterQueryProcessor, RasterResultDescriptor,
6
    SingleVectorSource, TypedRasterQueryProcessor, TypedVectorQueryProcessor, WorkflowOperatorPath,
7
};
8
use arrow::datatypes::ArrowNativeTypeOp;
9
use geoengine_datatypes::primitives::{CacheHint, ColumnSelection};
10

11
use crate::error;
12
use crate::processing::rasterization::GridOrDensity::Grid;
13
use crate::util;
14

15
use async_trait::async_trait;
16

17
use futures::stream::BoxStream;
18
use futures::{stream, StreamExt};
19
use geoengine_datatypes::collections::GeometryCollection;
20

21
use geoengine_datatypes::primitives::{
22
    AxisAlignedRectangle, BoundingBox2D, Coordinate2D, RasterQueryRectangle, SpatialPartition2D,
23
    SpatialPartitioned, SpatialResolution, VectorQueryRectangle,
24
};
25
use geoengine_datatypes::raster::{
26
    GeoTransform, Grid2D, GridOrEmpty, GridSize, GridSpaceToLinearSpace, RasterDataType,
27
    RasterTile2D, TilingSpecification,
28
};
29

30
use num_traits::FloatConst;
31
use rayon::prelude::*;
32

33
use serde::{Deserialize, Serialize};
34
use snafu::ensure;
35

36
use crate::util::{spawn_blocking, spawn_blocking_with_thread_pool};
37

38
use typetag::serde;
39

40
/// An operator that rasterizes vector data
41
pub type Rasterization = Operator<GridOrDensity, SingleVectorSource>;
42

43
impl OperatorName for Rasterization {
44
    const TYPE_NAME: &'static str = "Rasterization";
45
}
46

47
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)]
6✔
48
#[serde(rename_all = "camelCase")]
49
pub enum GridSizeMode {
50
    /// The spatial resolution is interpreted as a fixed size in coordinate units
51
    Fixed,
52
    /// The spatial resolution is interpreted as a multiplier for the query pixel size
53
    Relative,
54
}
55

56
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
8✔
57
#[serde(rename_all = "camelCase")]
58
#[serde(tag = "type")]
59
pub enum GridOrDensity {
60
    /// A grid which summarizes points in cells (2D histogram)
61
    Grid(GridParams),
62
    /// A heatmap calculated from a gaussian density function
63
    Density(DensityParams),
64
}
65

66
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
2✔
67
pub struct DensityParams {
68
    /// Defines the cutoff (as percentage of maximum density) down to which a point is taken
69
    /// into account for an output pixel density value
70
    cutoff: f64,
71
    /// The standard deviation parameter for the gaussian function
72
    stddev: f64,
73
}
74

75
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
6✔
76
#[serde(rename_all = "camelCase")]
77
pub struct GridParams {
78
    /// The size of grid cells, interpreted depending on the chosen grid size mode
79
    spatial_resolution: SpatialResolution,
80
    /// The origin coordinate which aligns the grid bounds
81
    origin_coordinate: Coordinate2D,
82
    /// Determines how to interpret the grid resolution
83
    grid_size_mode: GridSizeMode,
84
}
85

86
#[typetag::serde]
×
87
#[async_trait]
88
impl RasterOperator for Rasterization {
89
    async fn _initialize(
8✔
90
        self: Box<Self>,
8✔
91
        path: WorkflowOperatorPath,
8✔
92
        context: &dyn ExecutionContext,
8✔
93
    ) -> util::Result<Box<dyn InitializedRasterOperator>> {
8✔
94
        let name = CanonicOperatorName::from(&self);
8✔
95

96
        let initialized_source = self.sources.initialize_sources(path, context).await?;
8✔
97
        let vector_source = initialized_source.vector;
8✔
98
        let in_desc = vector_source.result_descriptor();
8✔
99

8✔
100
        let tiling_specification = context.tiling_specification();
8✔
101

8✔
102
        let out_desc = RasterResultDescriptor {
8✔
103
            spatial_reference: in_desc.spatial_reference,
8✔
104
            data_type: RasterDataType::F64,
8✔
105
            bbox: None,
8✔
106
            time: in_desc.time,
8✔
107
            resolution: None,
8✔
108
            bands: RasterBandDescriptors::new_single_band(),
8✔
109
        };
8✔
110

8✔
111
        match self.params {
8✔
112
            Grid(params) => Ok(InitializedGridRasterization {
6✔
113
                name,
6✔
114
                source: vector_source,
6✔
115
                result_descriptor: out_desc,
6✔
116
                spatial_resolution: params.spatial_resolution,
6✔
117
                grid_size_mode: params.grid_size_mode,
6✔
118
                tiling_specification,
6✔
119
                origin_coordinate: params.origin_coordinate,
6✔
120
            }
6✔
121
            .boxed()),
6✔
122
            GridOrDensity::Density(params) => InitializedDensityRasterization::new(
2✔
123
                name,
2✔
124
                vector_source,
2✔
125
                out_desc,
2✔
126
                tiling_specification,
2✔
127
                params.cutoff,
2✔
128
                params.stddev,
2✔
129
            )
2✔
130
            .map(InitializedRasterOperator::boxed),
2✔
131
        }
132
    }
16✔
133

134
    span_fn!(Rasterization);
×
135
}
136

137
pub struct InitializedGridRasterization {
138
    name: CanonicOperatorName,
139
    source: Box<dyn InitializedVectorOperator>,
140
    result_descriptor: RasterResultDescriptor,
141
    spatial_resolution: SpatialResolution,
142
    grid_size_mode: GridSizeMode,
143
    tiling_specification: TilingSpecification,
144
    origin_coordinate: Coordinate2D,
145
}
146

147
impl InitializedRasterOperator for InitializedGridRasterization {
148
    fn result_descriptor(&self) -> &RasterResultDescriptor {
×
149
        &self.result_descriptor
×
150
    }
×
151

152
    fn query_processor(&self) -> util::Result<TypedRasterQueryProcessor> {
6✔
153
        Ok(TypedRasterQueryProcessor::F64(
6✔
154
            GridRasterizationQueryProcessor {
6✔
155
                input: self.source.query_processor()?,
6✔
156
                spatial_resolution: self.spatial_resolution,
6✔
157
                grid_size_mode: self.grid_size_mode,
6✔
158
                tiling_specification: self.tiling_specification,
6✔
159
                origin_coordinate: self.origin_coordinate,
6✔
160
            }
6✔
161
            .boxed(),
6✔
162
        ))
163
    }
6✔
164

165
    fn canonic_name(&self) -> CanonicOperatorName {
×
166
        self.name.clone()
×
167
    }
×
168
}
169

170
pub struct InitializedDensityRasterization {
171
    name: CanonicOperatorName,
172
    source: Box<dyn InitializedVectorOperator>,
173
    result_descriptor: RasterResultDescriptor,
174
    tiling_specification: TilingSpecification,
175
    radius: f64,
176
    stddev: f64,
177
}
178

179
impl InitializedDensityRasterization {
180
    fn new(
2✔
181
        name: CanonicOperatorName,
2✔
182
        source: Box<dyn InitializedVectorOperator>,
2✔
183
        result_descriptor: RasterResultDescriptor,
2✔
184
        tiling_specification: TilingSpecification,
2✔
185
        cutoff: f64,
2✔
186
        stddev: f64,
2✔
187
    ) -> Result<Self, error::Error> {
2✔
188
        ensure!(
2✔
189
            (0. ..1.).contains(&cutoff),
2✔
190
            error::InvalidOperatorSpec {
×
191
                reason: "The cutoff for density rasterization must be in [0, 1).".to_string()
×
192
            }
×
193
        );
194
        ensure!(
2✔
195
            stddev >= 0.,
2✔
196
            error::InvalidOperatorSpec {
×
197
                reason: "The standard deviation for density rasterization must be greater than or equal to zero."
×
198
                    .to_string()
×
199
            }
×
200
        );
201

202
        // Determine radius from cutoff percentage
203
        let radius = gaussian_inverse(cutoff * gaussian(0., stddev), stddev);
2✔
204

2✔
205
        Ok(InitializedDensityRasterization {
2✔
206
            name,
2✔
207
            source,
2✔
208
            result_descriptor,
2✔
209
            tiling_specification,
2✔
210
            radius,
2✔
211
            stddev,
2✔
212
        })
2✔
213
    }
2✔
214
}
215

216
impl InitializedRasterOperator for InitializedDensityRasterization {
217
    fn result_descriptor(&self) -> &RasterResultDescriptor {
×
218
        &self.result_descriptor
×
219
    }
×
220

221
    fn query_processor(&self) -> util::Result<TypedRasterQueryProcessor> {
2✔
222
        Ok(TypedRasterQueryProcessor::F64(
2✔
223
            DensityRasterizationQueryProcessor {
2✔
224
                input: self.source.query_processor()?,
2✔
225
                tiling_specification: self.tiling_specification,
2✔
226
                radius: self.radius,
2✔
227
                stddev: self.stddev,
2✔
228
            }
2✔
229
            .boxed(),
2✔
230
        ))
231
    }
2✔
232

233
    fn canonic_name(&self) -> CanonicOperatorName {
×
234
        self.name.clone()
×
235
    }
×
236
}
237

238
pub struct GridRasterizationQueryProcessor {
239
    input: TypedVectorQueryProcessor,
240
    spatial_resolution: SpatialResolution,
241
    grid_size_mode: GridSizeMode,
242
    tiling_specification: TilingSpecification,
243
    origin_coordinate: Coordinate2D,
244
}
245

246
#[async_trait]
247
impl RasterQueryProcessor for GridRasterizationQueryProcessor {
248
    type RasterType = f64;
249

250
    /// Performs a grid rasterization by first determining the grid resolution to use.
251
    /// The grid resolution is limited to the query resolution, because a finer granularity
252
    /// would not be visible in the resulting raster.
253
    /// Then, for each tile, a grid, aligned to the configured origin coordinate, is created.
254
    /// All points within the spatial bounds of the grid are queried and counted in the
255
    /// grid cells.
256
    /// Finally, the grid resolution is upsampled (if necessary) to the tile resolution.
257
    async fn raster_query<'a>(
6✔
258
        &'a self,
6✔
259
        query: RasterQueryRectangle,
6✔
260
        ctx: &'a dyn QueryContext,
6✔
261
    ) -> util::Result<BoxStream<'a, util::Result<RasterTile2D<Self::RasterType>>>> {
6✔
262
        if let MultiPoint(points_processor) = &self.input {
6✔
263
            let grid_resolution = match self.grid_size_mode {
6✔
264
                GridSizeMode::Fixed => SpatialResolution {
3✔
265
                    x: f64::max(self.spatial_resolution.x, query.spatial_resolution.x),
3✔
266
                    y: f64::max(self.spatial_resolution.y, query.spatial_resolution.y),
3✔
267
                },
3✔
268
                GridSizeMode::Relative => SpatialResolution {
3✔
269
                    x: f64::max(
3✔
270
                        self.spatial_resolution.x * query.spatial_resolution.x,
3✔
271
                        query.spatial_resolution.x,
3✔
272
                    ),
3✔
273
                    y: f64::max(
3✔
274
                        self.spatial_resolution.y * query.spatial_resolution.y,
3✔
275
                        query.spatial_resolution.y,
3✔
276
                    ),
3✔
277
                },
3✔
278
            };
279

280
            let tiling_strategy = self
6✔
281
                .tiling_specification
6✔
282
                .strategy(query.spatial_resolution.x, -query.spatial_resolution.y);
6✔
283
            let tile_shape = tiling_strategy.tile_size_in_pixels;
6✔
284

6✔
285
            let tiles = stream::iter(
6✔
286
                tiling_strategy.tile_information_iterator(query.spatial_bounds),
6✔
287
            )
6✔
288
            .then(move |tile_info| async move {
24✔
289
                let grid_spatial_bounds = tile_info
24✔
290
                    .spatial_partition()
24✔
291
                    .snap_to_grid(self.origin_coordinate, grid_resolution);
24✔
292

24✔
293
                let grid_size_x =
24✔
294
                    f64::ceil(grid_spatial_bounds.size_x() / grid_resolution.x) as usize;
24✔
295
                let grid_size_y =
24✔
296
                    f64::ceil(grid_spatial_bounds.size_y() / grid_resolution.y) as usize;
24✔
297

24✔
298
                let vector_query = VectorQueryRectangle {
24✔
299
                    spatial_bounds: grid_spatial_bounds.as_bbox(),
24✔
300
                    time_interval: query.time_interval,
24✔
301
                    spatial_resolution: grid_resolution,
24✔
302
                    attributes: ColumnSelection::all(),
24✔
303
                };
24✔
304

24✔
305
                let grid_geo_transform = GeoTransform::new(
24✔
306
                    grid_spatial_bounds.upper_left(),
24✔
307
                    grid_resolution.x,
24✔
308
                    -grid_resolution.y,
24✔
309
                );
24✔
310

311
                let mut chunks = points_processor.query(vector_query, ctx).await?;
24✔
312

313
                let mut cache_hint = CacheHint::max_duration();
24✔
314

24✔
315
                let mut grid_data = vec![0.; grid_size_x * grid_size_y];
24✔
316
                while let Some(chunk) = chunks.next().await {
48✔
317
                    let chunk = chunk?;
24✔
318

319
                    cache_hint.merge_with(&chunk.cache_hint);
24✔
320

321
                    grid_data = spawn_blocking(move || {
24✔
322
                        for &coord in chunk.coordinates() {
24✔
323
                            if !grid_spatial_bounds.contains_coordinate(&coord) {
24✔
324
                                continue;
3✔
325
                            }
21✔
326
                            let [y, x] = grid_geo_transform.coordinate_to_grid_idx_2d(coord).0;
21✔
327
                            grid_data[x as usize + y as usize * grid_size_x] += 1.;
21✔
328
                        }
329
                        grid_data
24✔
330
                    })
24✔
331
                    .await
24✔
332
                    .expect("Should only forward panics from spawned task");
24✔
333
                }
334

335
                let tile_data = spawn_blocking(move || {
24✔
336
                    let mut tile_data = Vec::with_capacity(tile_shape.number_of_elements());
24✔
337
                    for tile_y in 0..tile_shape.axis_size_y() as isize {
60✔
338
                        for tile_x in 0..tile_shape.axis_size_x() as isize {
156✔
339
                            let pixel_coordinate = tile_info
156✔
340
                                .tile_geo_transform()
156✔
341
                                .grid_idx_to_pixel_center_coordinate_2d([tile_y, tile_x].into());
156✔
342
                            if query.spatial_bounds.contains_coordinate(&pixel_coordinate) {
156✔
343
                                let [grid_y, grid_x] = grid_geo_transform
156✔
344
                                    .coordinate_to_grid_idx_2d(pixel_coordinate)
156✔
345
                                    .0;
156✔
346
                                tile_data.push(
156✔
347
                                    grid_data[grid_x as usize + grid_y as usize * grid_size_x],
156✔
348
                                );
156✔
349
                            } else {
156✔
350
                                tile_data.push(0.);
×
351
                            }
×
352
                        }
353
                    }
354
                    tile_data
24✔
355
                })
24✔
356
                .await
24✔
357
                .expect("Should only forward panics from spawned task");
24✔
358
                let tile_grid = Grid2D::new(tile_shape, tile_data)
24✔
359
                    .expect("Data vector length should match the number of pixels in the tile");
24✔
360

24✔
361
                Ok(RasterTile2D::new_with_tile_info(
24✔
362
                    query.time_interval,
24✔
363
                    tile_info,
24✔
364
                    0,
24✔
365
                    GridOrEmpty::Grid(tile_grid.into()),
24✔
366
                    cache_hint,
24✔
367
                ))
24✔
368
            });
24✔
369
            Ok(tiles.boxed())
6✔
370
        } else {
NEW
371
            Ok(generate_zeroed_tiles(self.tiling_specification, &query))
×
372
        }
373
    }
12✔
374
}
375

376
pub struct DensityRasterizationQueryProcessor {
377
    input: TypedVectorQueryProcessor,
378
    tiling_specification: TilingSpecification,
379
    radius: f64,
380
    stddev: f64,
381
}
382

383
#[async_trait]
384
impl RasterQueryProcessor for DensityRasterizationQueryProcessor {
385
    type RasterType = f64;
386

387
    /// Performs a gaussian density rasterization.
388
    /// For each tile, the spatial bounds are extended by `radius` in x and y direction.
389
    /// All points within these extended bounds are then queried. For each point, the distance to
390
    /// its surrounding tile pixels (up to `radius` distance) is measured and input into the
391
    /// gaussian density function with the configured standard deviation. The density values
392
    /// for each pixel are then summed to result in the tile pixel grid.
393
    async fn raster_query<'a>(
2✔
394
        &'a self,
2✔
395
        query: RasterQueryRectangle,
2✔
396
        ctx: &'a dyn QueryContext,
2✔
397
    ) -> util::Result<BoxStream<'a, util::Result<RasterTile2D<Self::RasterType>>>> {
2✔
398
        if let MultiPoint(points_processor) = &self.input {
2✔
399
            let tiling_strategy = self
2✔
400
                .tiling_specification
2✔
401
                .strategy(query.spatial_resolution.x, -query.spatial_resolution.y);
2✔
402

2✔
403
            let tile_size_x = tiling_strategy.tile_size_in_pixels.axis_size_x();
2✔
404
            let tile_size_y = tiling_strategy.tile_size_in_pixels.axis_size_y();
2✔
405

2✔
406
            // Use rounding factor calculated from query resolution to extend in full pixel units
2✔
407
            let rounding_factor = f64::max(
2✔
408
                1. / query.spatial_resolution.x,
2✔
409
                1. / query.spatial_resolution.y,
2✔
410
            );
2✔
411
            let radius = (self.radius * rounding_factor).ceil() / rounding_factor;
2✔
412

2✔
413
            let tiles = stream::iter(
2✔
414
                tiling_strategy.tile_information_iterator(query.spatial_bounds),
2✔
415
            )
2✔
416
            .then(move |tile_info| async move {
4✔
417
                let tile_bounds = tile_info.spatial_partition();
4✔
418

4✔
419
                let vector_query = VectorQueryRectangle {
4✔
420
                    spatial_bounds: extended_bounding_box_from_spatial_partition(
4✔
421
                        tile_bounds,
4✔
422
                        radius,
4✔
423
                    ),
4✔
424
                    time_interval: query.time_interval,
4✔
425
                    spatial_resolution: query.spatial_resolution,
4✔
426
                    attributes: ColumnSelection::all(),
4✔
427
                };
4✔
428

4✔
429
                let tile_geo_transform = tile_info.tile_geo_transform();
4✔
430

431
                let mut chunks = points_processor.query(vector_query, ctx).await?;
4✔
432

433
                let mut tile_data = vec![0.; tile_size_x * tile_size_y];
4✔
434

4✔
435
                let mut cache_hint = CacheHint::max_duration();
4✔
436

437
                while let Some(chunk) = chunks.next().await {
8✔
438
                    let chunk = chunk?;
4✔
439

440
                    cache_hint.merge_with(&chunk.cache_hint);
4✔
441

4✔
442
                    let stddev = self.stddev;
4✔
443
                    tile_data =
4✔
444
                        spawn_blocking_with_thread_pool(ctx.thread_pool().clone(), move || {
4✔
445
                            tile_data.par_iter_mut().enumerate().for_each(
4✔
446
                                |(linear_index, pixel)| {
16✔
447
                                    let pixel_coordinate = tile_geo_transform
16✔
448
                                        .grid_idx_to_pixel_center_coordinate_2d(
16✔
449
                                            tile_geo_transform
16✔
450
                                                .spatial_to_grid_bounds(&tile_bounds)
16✔
451
                                                .grid_idx_unchecked(linear_index),
16✔
452
                                        );
16✔
453

454
                                    for coord in chunk.coordinates() {
32✔
455
                                        let distance = coord.euclidean_distance(&pixel_coordinate);
32✔
456

32✔
457
                                        if distance <= radius {
32✔
458
                                            *pixel += gaussian(distance, stddev);
20✔
459
                                        }
20✔
460
                                    }
461
                                },
16✔
462
                            );
4✔
463

4✔
464
                            tile_data
4✔
465
                        })
4✔
466
                        .await?;
4✔
467
                }
468

469
                Ok(RasterTile2D::new_with_tile_info(
4✔
470
                    query.time_interval,
4✔
471
                    tile_info,
4✔
472
                    0,
4✔
473
                    GridOrEmpty::Grid(
4✔
474
                        Grid2D::new(tiling_strategy.tile_size_in_pixels, tile_data)
4✔
475
                            .expect(
4✔
476
                                "Data vector length should match the number of pixels in the tile",
4✔
477
                            )
4✔
478
                            .into(),
4✔
479
                    ),
4✔
480
                    cache_hint,
4✔
481
                ))
4✔
482
            });
4✔
483

2✔
484
            Ok(tiles.boxed())
2✔
485
        } else {
NEW
486
            Ok(generate_zeroed_tiles(self.tiling_specification, &query))
×
487
        }
488
    }
4✔
489
}
490

491
fn generate_zeroed_tiles<'a>(
×
492
    tiling_specification: TilingSpecification,
×
NEW
493
    query: &RasterQueryRectangle,
×
494
) -> BoxStream<'a, util::Result<RasterTile2D<f64>>> {
×
495
    let tiling_strategy =
×
496
        tiling_specification.strategy(query.spatial_resolution.x, -query.spatial_resolution.y);
×
497
    let tile_shape = tiling_strategy.tile_size_in_pixels;
×
NEW
498
    let time_interval = query.time_interval;
×
499

×
500
    stream::iter(
×
501
        tiling_strategy
×
502
            .tile_information_iterator(query.spatial_bounds)
×
503
            .map(move |tile_info| {
×
504
                let tile_data = vec![0.; tile_shape.number_of_elements()];
×
505
                let tile_grid = Grid2D::new(tile_shape, tile_data)
×
506
                    .expect("Data vector length should match the number of pixels in the tile");
×
507

×
508
                Ok(RasterTile2D::new_with_tile_info(
×
NEW
509
                    time_interval,
×
510
                    tile_info,
×
NEW
511
                    0,
×
512
                    GridOrEmpty::Grid(tile_grid.into()),
×
513
                    CacheHint::no_cache(),
×
514
                ))
×
515
            }),
×
516
    )
×
517
    .boxed()
×
518
}
×
519

520
fn extended_bounding_box_from_spatial_partition(
4✔
521
    spatial_partition: SpatialPartition2D,
4✔
522
    extent: f64,
4✔
523
) -> BoundingBox2D {
4✔
524
    BoundingBox2D::new_unchecked(
4✔
525
        Coordinate2D::new(
4✔
526
            spatial_partition
4✔
527
                .lower_left()
4✔
528
                .x
4✔
529
                .sub_checked(extent)
4✔
530
                .unwrap_or(f64::MIN),
4✔
531
            spatial_partition
4✔
532
                .lower_left()
4✔
533
                .y
4✔
534
                .sub_checked(extent)
4✔
535
                .unwrap_or(f64::MIN),
4✔
536
        ),
4✔
537
        Coordinate2D::new(
4✔
538
            spatial_partition
4✔
539
                .upper_right()
4✔
540
                .x
4✔
541
                .add_checked(extent)
4✔
542
                .unwrap_or(f64::MAX),
4✔
543
            spatial_partition
4✔
544
                .upper_right()
4✔
545
                .y
4✔
546
                .add_checked(extent)
4✔
547
                .unwrap_or(f64::MAX),
4✔
548
        ),
4✔
549
    )
4✔
550
}
4✔
551

552
/// Calculates the gaussian density value for
553
/// `x`, the distance from the mean and
554
/// `stddev`, the standard deviation
555
fn gaussian(x: f64, stddev: f64) -> f64 {
46✔
556
    (1. / (f64::sqrt(2. * f64::PI()) * stddev)) * f64::exp(-(x * x) / (2. * stddev * stddev))
46✔
557
}
46✔
558

559
/// The inverse function of [gaussian](gaussian)
560
fn gaussian_inverse(x: f64, stddev: f64) -> f64 {
2✔
561
    f64::sqrt(2.)
2✔
562
        * f64::sqrt(stddev * stddev * f64::ln(1. / (f64::sqrt(2. * f64::PI()) * stddev * x)))
2✔
563
}
2✔
564

565
#[cfg(test)]
566
mod tests {
567
    use crate::engine::{
568
        InitializedRasterOperator, MockExecutionContext, MockQueryContext, QueryProcessor,
569
        RasterOperator, SingleVectorSource, VectorOperator, WorkflowOperatorPath,
570
    };
571
    use crate::mock::{MockPointSource, MockPointSourceParams};
572
    use crate::processing::rasterization::GridSizeMode::{Fixed, Relative};
573
    use crate::processing::rasterization::{
574
        gaussian, DensityParams, GridOrDensity, GridParams, Rasterization,
575
    };
576
    use futures::StreamExt;
577
    use geoengine_datatypes::primitives::{
578
        BandSelection, Coordinate2D, RasterQueryRectangle, SpatialPartition2D, SpatialResolution,
579
    };
580
    use geoengine_datatypes::raster::TilingSpecification;
581
    use geoengine_datatypes::util::test::TestDefault;
582

583
    async fn get_results(
8✔
584
        rasterization: Box<dyn InitializedRasterOperator>,
8✔
585
        query: RasterQueryRectangle,
8✔
586
    ) -> Vec<Vec<f64>> {
8✔
587
        rasterization
8✔
588
            .query_processor()
8✔
589
            .unwrap()
8✔
590
            .get_f64()
8✔
591
            .unwrap()
8✔
592
            .query(query, &MockQueryContext::test_default())
8✔
593
            .await
×
594
            .unwrap()
8✔
595
            .map(|res| {
28✔
596
                res.unwrap()
28✔
597
                    .grid_array
28✔
598
                    .into_materialized_masked_grid()
28✔
599
                    .inner_grid
28✔
600
                    .data
28✔
601
            })
28✔
602
            .collect()
8✔
603
            .await
52✔
604
    }
8✔
605

606
    #[tokio::test]
1✔
607
    async fn fixed_grid_basic() {
1✔
608
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
609
            TilingSpecification::new([0., 0.].into(), [2, 2].into()),
1✔
610
        );
1✔
611
        let rasterization = Rasterization {
1✔
612
            params: GridOrDensity::Grid(GridParams {
1✔
613
                spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
614
                origin_coordinate: [0., 0.].into(),
1✔
615
                grid_size_mode: Fixed,
1✔
616
            }),
1✔
617
            sources: SingleVectorSource {
1✔
618
                vector: MockPointSource {
1✔
619
                    params: MockPointSourceParams {
1✔
620
                        points: vec![
1✔
621
                            (-1., 1.).into(),
1✔
622
                            (1., 1.).into(),
1✔
623
                            (-1., -1.).into(),
1✔
624
                            (1., -1.).into(),
1✔
625
                        ],
1✔
626
                    },
1✔
627
                }
1✔
628
                .boxed(),
1✔
629
            },
1✔
630
        }
1✔
631
        .boxed()
1✔
632
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
633
        .await
×
634
        .unwrap();
1✔
635

1✔
636
        let query = RasterQueryRectangle {
1✔
637
            spatial_bounds: SpatialPartition2D::new([-2., 2.].into(), [2., -2.].into()).unwrap(),
1✔
638
            time_interval: Default::default(),
1✔
639
            spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
640
            attributes: BandSelection::first(),
1✔
641
        };
1✔
642

643
        let res = get_results(rasterization, query).await;
8✔
644

645
        assert_eq!(
1✔
646
            res,
1✔
647
            vec![
1✔
648
                vec![0., 0., 0., 1.],
1✔
649
                vec![0., 0., 0., 1.],
1✔
650
                vec![0., 0., 0., 1.],
1✔
651
                vec![0., 0., 0., 1.],
1✔
652
            ]
1✔
653
        );
1✔
654
    }
655

656
    #[tokio::test]
1✔
657
    async fn fixed_grid_with_shift() {
1✔
658
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
659
            TilingSpecification::new([0., 0.].into(), [2, 2].into()),
1✔
660
        );
1✔
661
        let rasterization = Rasterization {
1✔
662
            params: GridOrDensity::Grid(GridParams {
1✔
663
                spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
664
                origin_coordinate: [0.5, -0.5].into(),
1✔
665
                grid_size_mode: Fixed,
1✔
666
            }),
1✔
667
            sources: SingleVectorSource {
1✔
668
                vector: MockPointSource {
1✔
669
                    params: MockPointSourceParams {
1✔
670
                        points: vec![
1✔
671
                            (-1., 1.).into(),
1✔
672
                            (1., 1.).into(),
1✔
673
                            (-1., -1.).into(),
1✔
674
                            (1., -1.).into(),
1✔
675
                        ],
1✔
676
                    },
1✔
677
                }
1✔
678
                .boxed(),
1✔
679
            },
1✔
680
        }
1✔
681
        .boxed()
1✔
682
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
683
        .await
×
684
        .unwrap();
1✔
685

1✔
686
        let query = RasterQueryRectangle {
1✔
687
            spatial_bounds: SpatialPartition2D::new([-2., 2.].into(), [2., -2.].into()).unwrap(),
1✔
688
            time_interval: Default::default(),
1✔
689
            spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
690
            attributes: BandSelection::first(),
1✔
691
        };
1✔
692

693
        let res = get_results(rasterization, query).await;
8✔
694

695
        assert_eq!(
1✔
696
            res,
1✔
697
            vec![
1✔
698
                vec![1., 0., 0., 0.],
1✔
699
                vec![1., 0., 0., 0.],
1✔
700
                vec![1., 0., 0., 0.],
1✔
701
                vec![1., 0., 0., 0.],
1✔
702
            ]
1✔
703
        );
1✔
704
    }
705

706
    #[tokio::test]
1✔
707
    async fn fixed_grid_with_upsampling() {
1✔
708
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
709
            TilingSpecification::new([0., 0.].into(), [3, 3].into()),
1✔
710
        );
1✔
711
        let rasterization = Rasterization {
1✔
712
            params: GridOrDensity::Grid(GridParams {
1✔
713
                spatial_resolution: SpatialResolution { x: 2.0, y: 2.0 },
1✔
714
                origin_coordinate: [0., 0.].into(),
1✔
715
                grid_size_mode: Fixed,
1✔
716
            }),
1✔
717
            sources: SingleVectorSource {
1✔
718
                vector: MockPointSource {
1✔
719
                    params: MockPointSourceParams {
1✔
720
                        points: vec![
1✔
721
                            (-1., 1.).into(),
1✔
722
                            (1., 1.).into(),
1✔
723
                            (-1., -1.).into(),
1✔
724
                            (1., -1.).into(),
1✔
725
                        ],
1✔
726
                    },
1✔
727
                }
1✔
728
                .boxed(),
1✔
729
            },
1✔
730
        }
1✔
731
        .boxed()
1✔
732
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
733
        .await
×
734
        .unwrap();
1✔
735

1✔
736
        let query = RasterQueryRectangle {
1✔
737
            spatial_bounds: SpatialPartition2D::new([-3., 3.].into(), [3., -3.].into()).unwrap(),
1✔
738
            time_interval: Default::default(),
1✔
739
            spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
740
            attributes: BandSelection::first(),
1✔
741
        };
1✔
742

743
        let res = get_results(rasterization, query).await;
8✔
744

745
        assert_eq!(
1✔
746
            res,
1✔
747
            vec![
1✔
748
                vec![0., 0., 0., 0., 1., 1., 0., 1., 1.],
1✔
749
                vec![0., 0., 0., 1., 1., 0., 1., 1., 0.],
1✔
750
                vec![0., 1., 1., 0., 1., 1., 0., 0., 0.],
1✔
751
                vec![1., 1., 0., 1., 1., 0., 0., 0., 0.],
1✔
752
            ]
1✔
753
        );
1✔
754
    }
755

756
    #[tokio::test]
1✔
757
    async fn relative_grid_basic() {
1✔
758
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
759
            TilingSpecification::new([0., 0.].into(), [3, 3].into()),
1✔
760
        );
1✔
761
        let rasterization = Rasterization {
1✔
762
            params: GridOrDensity::Grid(GridParams {
1✔
763
                spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
764
                origin_coordinate: [0., 0.].into(),
1✔
765
                grid_size_mode: Relative,
1✔
766
            }),
1✔
767
            sources: SingleVectorSource {
1✔
768
                vector: MockPointSource {
1✔
769
                    params: MockPointSourceParams {
1✔
770
                        points: vec![
1✔
771
                            (-1., 1.).into(),
1✔
772
                            (1., 1.).into(),
1✔
773
                            (-1., -1.).into(),
1✔
774
                            (1., -1.).into(),
1✔
775
                        ],
1✔
776
                    },
1✔
777
                }
1✔
778
                .boxed(),
1✔
779
            },
1✔
780
        }
1✔
781
        .boxed()
1✔
782
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
783
        .await
×
784
        .unwrap();
1✔
785

1✔
786
        let query = RasterQueryRectangle {
1✔
787
            spatial_bounds: SpatialPartition2D::new([-1.5, 1.5].into(), [1.5, -1.5].into())
1✔
788
                .unwrap(),
1✔
789
            time_interval: Default::default(),
1✔
790
            spatial_resolution: SpatialResolution { x: 0.5, y: 0.5 },
1✔
791
            attributes: BandSelection::first(),
1✔
792
        };
1✔
793

794
        let res = get_results(rasterization, query).await;
8✔
795

796
        assert_eq!(
1✔
797
            res,
1✔
798
            vec![
1✔
799
                vec![0., 0., 0., 0., 1., 0., 0., 0., 0.],
1✔
800
                vec![0., 0., 0., 0., 0., 1., 0., 0., 0.],
1✔
801
                vec![0., 0., 0., 0., 0., 0., 0., 1., 0.],
1✔
802
                vec![0., 0., 0., 0., 0., 0., 0., 0., 1.],
1✔
803
            ]
1✔
804
        );
1✔
805
    }
806

807
    #[tokio::test]
1✔
808
    async fn relative_grid_with_shift() {
1✔
809
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
810
            TilingSpecification::new([0., 0.].into(), [3, 3].into()),
1✔
811
        );
1✔
812
        let rasterization = Rasterization {
1✔
813
            params: GridOrDensity::Grid(GridParams {
1✔
814
                spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
815
                origin_coordinate: [0.25, -0.25].into(),
1✔
816
                grid_size_mode: Relative,
1✔
817
            }),
1✔
818
            sources: SingleVectorSource {
1✔
819
                vector: MockPointSource {
1✔
820
                    params: MockPointSourceParams {
1✔
821
                        points: vec![
1✔
822
                            (-1., 1.).into(),
1✔
823
                            (1., 1.).into(),
1✔
824
                            (-1., -1.).into(),
1✔
825
                            (1., -1.).into(),
1✔
826
                        ],
1✔
827
                    },
1✔
828
                }
1✔
829
                .boxed(),
1✔
830
            },
1✔
831
        }
1✔
832
        .boxed()
1✔
833
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
834
        .await
×
835
        .unwrap();
1✔
836

1✔
837
        let query = RasterQueryRectangle {
1✔
838
            spatial_bounds: SpatialPartition2D::new([-1.5, 1.5].into(), [1.5, -1.5].into())
1✔
839
                .unwrap(),
1✔
840
            time_interval: Default::default(),
1✔
841
            spatial_resolution: SpatialResolution { x: 0.5, y: 0.5 },
1✔
842
            attributes: BandSelection::first(),
1✔
843
        };
1✔
844

845
        let res = get_results(rasterization, query).await;
8✔
846

847
        assert_eq!(
1✔
848
            res,
1✔
849
            vec![
1✔
850
                vec![1., 0., 0., 0., 0., 0., 0., 0., 0.],
1✔
851
                vec![0., 1., 0., 0., 0., 0., 0., 0., 0.],
1✔
852
                vec![0., 0., 0., 1., 0., 0., 0., 0., 0.],
1✔
853
                vec![0., 0., 0., 0., 1., 0., 0., 0., 0.],
1✔
854
            ]
1✔
855
        );
1✔
856
    }
857

858
    #[tokio::test]
1✔
859
    async fn relative_grid_with_upsampling() {
1✔
860
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
861
            TilingSpecification::new([0., 0.].into(), [2, 2].into()),
1✔
862
        );
1✔
863
        let rasterization = Rasterization {
1✔
864
            params: GridOrDensity::Grid(GridParams {
1✔
865
                spatial_resolution: SpatialResolution { x: 2.0, y: 2.0 },
1✔
866
                origin_coordinate: [0., 0.].into(),
1✔
867
                grid_size_mode: Relative,
1✔
868
            }),
1✔
869
            sources: SingleVectorSource {
1✔
870
                vector: MockPointSource {
1✔
871
                    params: MockPointSourceParams {
1✔
872
                        points: vec![
1✔
873
                            (-1., 1.).into(),
1✔
874
                            (1., 1.).into(),
1✔
875
                            (-1., -1.).into(),
1✔
876
                            (1., -1.).into(),
1✔
877
                        ],
1✔
878
                    },
1✔
879
                }
1✔
880
                .boxed(),
1✔
881
            },
1✔
882
        }
1✔
883
        .boxed()
1✔
884
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
885
        .await
×
886
        .unwrap();
1✔
887

1✔
888
        let query = RasterQueryRectangle {
1✔
889
            spatial_bounds: SpatialPartition2D::new([-1., 1.].into(), [1., -1.].into()).unwrap(),
1✔
890
            time_interval: Default::default(),
1✔
891
            spatial_resolution: SpatialResolution { x: 0.5, y: 0.5 },
1✔
892
            attributes: BandSelection::first(),
1✔
893
        };
1✔
894

895
        let res = get_results(rasterization, query).await;
8✔
896

897
        assert_eq!(
1✔
898
            res,
1✔
899
            vec![
1✔
900
                vec![1., 1., 1., 1.],
1✔
901
                vec![0., 0., 0., 0.],
1✔
902
                vec![0., 0., 0., 0.],
1✔
903
                vec![0., 0., 0., 0.]
1✔
904
            ]
1✔
905
        );
1✔
906
    }
907

908
    #[tokio::test]
1✔
909
    async fn density_basic() {
1✔
910
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
911
            TilingSpecification::new([0., 0.].into(), [2, 2].into()),
1✔
912
        );
1✔
913
        let rasterization = Rasterization {
1✔
914
            params: GridOrDensity::Density(DensityParams {
1✔
915
                cutoff: gaussian(0.99, 1.0) / gaussian(0., 1.0),
1✔
916
                stddev: 1.0,
1✔
917
            }),
1✔
918
            sources: SingleVectorSource {
1✔
919
                vector: MockPointSource {
1✔
920
                    params: MockPointSourceParams {
1✔
921
                        points: vec![(-1., 1.).into(), (1., 1.).into()],
1✔
922
                    },
1✔
923
                }
1✔
924
                .boxed(),
1✔
925
            },
1✔
926
        }
1✔
927
        .boxed()
1✔
928
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
929
        .await
×
930
        .unwrap();
1✔
931

1✔
932
        let query = RasterQueryRectangle {
1✔
933
            spatial_bounds: SpatialPartition2D::new([-2., 2.].into(), [2., 0.].into()).unwrap(),
1✔
934
            time_interval: Default::default(),
1✔
935
            spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
936
            attributes: BandSelection::first(),
1✔
937
        };
1✔
938

939
        let res = get_results(rasterization, query).await;
2✔
940

941
        assert_eq!(
1✔
942
            res,
1✔
943
            vec![
1✔
944
                vec![
1✔
945
                    gaussian(
1✔
946
                        Coordinate2D::new(-1., 1.)
1✔
947
                            .euclidean_distance(&Coordinate2D::new(-1.5, 1.5)),
1✔
948
                        1.0
1✔
949
                    ),
1✔
950
                    gaussian(
1✔
951
                        Coordinate2D::new(-1., 1.)
1✔
952
                            .euclidean_distance(&Coordinate2D::new(-0.5, 1.5)),
1✔
953
                        1.0
1✔
954
                    ),
1✔
955
                    gaussian(
1✔
956
                        Coordinate2D::new(-1., 1.)
1✔
957
                            .euclidean_distance(&Coordinate2D::new(-1.5, 0.5)),
1✔
958
                        1.0
1✔
959
                    ),
1✔
960
                    gaussian(
1✔
961
                        Coordinate2D::new(-1., 1.)
1✔
962
                            .euclidean_distance(&Coordinate2D::new(-0.5, 0.5)),
1✔
963
                        1.0
1✔
964
                    )
1✔
965
                ],
1✔
966
                vec![
1✔
967
                    gaussian(
1✔
968
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(0.5, 1.5)),
1✔
969
                        1.0
1✔
970
                    ),
1✔
971
                    gaussian(
1✔
972
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(1.5, 1.5)),
1✔
973
                        1.0
1✔
974
                    ),
1✔
975
                    gaussian(
1✔
976
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(0.5, 0.5)),
1✔
977
                        1.0
1✔
978
                    ),
1✔
979
                    gaussian(
1✔
980
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(1.5, 0.5)),
1✔
981
                        1.0
1✔
982
                    )
1✔
983
                ],
1✔
984
            ]
1✔
985
        );
1✔
986
    }
987

988
    #[tokio::test]
1✔
989
    async fn density_radius_overlap() {
1✔
990
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
991
            TilingSpecification::new([0., 0.].into(), [2, 2].into()),
1✔
992
        );
1✔
993
        let rasterization = Rasterization {
1✔
994
            params: GridOrDensity::Density(DensityParams {
1✔
995
                cutoff: gaussian(1.99, 1.0) / gaussian(0., 1.0),
1✔
996
                stddev: 1.0,
1✔
997
            }),
1✔
998
            sources: SingleVectorSource {
1✔
999
                vector: MockPointSource {
1✔
1000
                    params: MockPointSourceParams {
1✔
1001
                        points: vec![(-1., 1.).into(), (1., 1.).into()],
1✔
1002
                    },
1✔
1003
                }
1✔
1004
                .boxed(),
1✔
1005
            },
1✔
1006
        }
1✔
1007
        .boxed()
1✔
1008
        .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1009
        .await
×
1010
        .unwrap();
1✔
1011

1✔
1012
        let query = RasterQueryRectangle {
1✔
1013
            spatial_bounds: SpatialPartition2D::new([-2., 2.].into(), [2., 0.].into()).unwrap(),
1✔
1014
            time_interval: Default::default(),
1✔
1015
            spatial_resolution: SpatialResolution { x: 1.0, y: 1.0 },
1✔
1016
            attributes: BandSelection::first(),
1✔
1017
        };
1✔
1018

1019
        let res = get_results(rasterization, query).await;
2✔
1020

1021
        assert_eq!(
1✔
1022
            res,
1✔
1023
            vec![
1✔
1024
                vec![
1✔
1025
                    gaussian(
1✔
1026
                        Coordinate2D::new(-1., 1.)
1✔
1027
                            .euclidean_distance(&Coordinate2D::new(-1.5, 1.5)),
1✔
1028
                        1.0
1✔
1029
                    ),
1✔
1030
                    gaussian(
1✔
1031
                        Coordinate2D::new(-1., 1.)
1✔
1032
                            .euclidean_distance(&Coordinate2D::new(-0.5, 1.5)),
1✔
1033
                        1.0
1✔
1034
                    ) + gaussian(
1✔
1035
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(-0.5, 1.5)),
1✔
1036
                        1.0
1✔
1037
                    ),
1✔
1038
                    gaussian(
1✔
1039
                        Coordinate2D::new(-1., 1.)
1✔
1040
                            .euclidean_distance(&Coordinate2D::new(-1.5, 0.5)),
1✔
1041
                        1.0
1✔
1042
                    ),
1✔
1043
                    gaussian(
1✔
1044
                        Coordinate2D::new(-1., 1.)
1✔
1045
                            .euclidean_distance(&Coordinate2D::new(-0.5, 0.5)),
1✔
1046
                        1.0
1✔
1047
                    ) + gaussian(
1✔
1048
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(-0.5, 0.5)),
1✔
1049
                        1.0
1✔
1050
                    )
1✔
1051
                ],
1✔
1052
                vec![
1✔
1053
                    gaussian(
1✔
1054
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(0.5, 1.5)),
1✔
1055
                        1.0
1✔
1056
                    ) + gaussian(
1✔
1057
                        Coordinate2D::new(-1., 1.).euclidean_distance(&Coordinate2D::new(0.5, 1.5)),
1✔
1058
                        1.0
1✔
1059
                    ),
1✔
1060
                    gaussian(
1✔
1061
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(1.5, 1.5)),
1✔
1062
                        1.0
1✔
1063
                    ),
1✔
1064
                    gaussian(
1✔
1065
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(0.5, 0.5)),
1✔
1066
                        1.0
1✔
1067
                    ) + gaussian(
1✔
1068
                        Coordinate2D::new(-1., 1.).euclidean_distance(&Coordinate2D::new(0.5, 0.5)),
1✔
1069
                        1.0
1✔
1070
                    ),
1✔
1071
                    gaussian(
1✔
1072
                        Coordinate2D::new(1., 1.).euclidean_distance(&Coordinate2D::new(1.5, 0.5)),
1✔
1073
                        1.0
1✔
1074
                    )
1✔
1075
                ],
1✔
1076
            ]
1✔
1077
        );
1✔
1078
    }
1079
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc