Coverage for sarvey/config.py: 77%
295 statements
« prev ^ index » next coverage.py v7.6.0, created at 2024-10-17 12:36 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2024-10-17 12:36 +0000
1#!/usr/bin/env python
3# SARvey - A multitemporal InSAR time series tool for the derivation of displacements.
4#
5# Copyright (C) 2021-2024 Andreas Piter (IPI Hannover, piter@ipi.uni-hannover.de)
6#
7# This software was developed together with FERN.Lab (fernlab@gfz-potsdam.de) in the context
8# of the SAR4Infra project with funds of the German Federal Ministry for Digital and
9# Transport and contributions from Landesamt fuer Vermessung und Geoinformation
10# Schleswig-Holstein and Landesbetrieb Strassenbau und Verkehr Schleswig-Holstein.
11#
12# This program is free software: you can redistribute it and/or modify it under
13# the terms of the GNU General Public License as published by the Free Software
14# Foundation, either version 3 of the License, or (at your option) any later
15# version.
16#
17# Important: This package uses PyMaxFlow. The core of PyMaxflows library is the C++
18# implementation by Vladimir Kolmogorov. It is also licensed under the GPL, but it REQUIRES that you
19# cite [BOYKOV04] (see LICENSE) in any resulting publication if you use this code for research purposes.
20# This requirement extends to SARvey.
21#
22# This program is distributed in the hope that it will be useful, but WITHOUT
23# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
24# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
25# details.
26#
27# You should have received a copy of the GNU Lesser General Public License along
28# with this program. If not, see <https://www.gnu.org/licenses/>.
30"""Configuration module for SARvey."""
31import os
32import json5
33from datetime import date
34from json import JSONDecodeError
35from typing import Optional
36from pydantic import BaseModel, Field, validator, Extra
39class General(BaseModel, extra=Extra.forbid):
40 """Template for settings in config file."""
42 input_path: str = Field(
43 title="The path to the input data directory.",
44 description="Set the path of the input data directory.",
45 default="inputs/"
46 )
48 output_path: str = Field(
49 title="The path to the processing output data directory.",
50 description="Set the path of the processing output data directory.",
51 default="outputs/"
52 )
54 num_cores: int = Field(
55 title="Number of cores",
56 description="Set the number of cores for parallel processing.",
57 default=50
58 )
60 num_patches: int = Field(
61 title="Number of patches",
62 description="Set the number of patches for processing large areas patch-wise.",
63 default=1
64 )
66 apply_temporal_unwrapping: bool = Field(
67 title="Apply temporal unwrapping",
68 description="Apply temporal unwrapping additionally to spatial unwrapping.",
69 default=True
70 )
72 spatial_unwrapping_method: str = Field(
73 title="Spatial unwrapping method",
74 description="Select spatial unwrapping method from 'ilp' and 'puma'.",
75 default='puma'
76 )
78 logging_level: str = Field(
79 title="Logging level.",
80 description="Set loggig level.",
81 default="INFO"
82 )
84 logfile_path: str = Field(
85 title="Logfile Path.",
86 description="Path to directory where the logfiles should be saved.",
87 default="logfiles/"
88 )
90 @validator('input_path')
91 def checkPathInputs(cls, v):
92 """Check if the input path exists."""
93 if v == "":
94 raise ValueError("Empty string is not allowed.")
95 if not os.path.exists(os.path.abspath(v)):
96 raise ValueError(f"input_path is invalid: {os.path.abspath(v)}")
97 if not os.path.exists(os.path.join(os.path.abspath(v), "slcStack.h5")):
98 raise ValueError(f"'slcStack.h5' does not exist: {v}")
99 if not os.path.exists(os.path.join(os.path.abspath(v), "geometryRadar.h5")):
100 raise ValueError(f"'geometryRadar.h5' does not exist: {v}")
101 return v
103 @validator('num_cores')
104 def checkNumCores(cls, v):
105 """Check if the number of cores is valid."""
106 if v <= 0:
107 raise ValueError("Number of cores must be greater than zero.")
108 return v
110 @validator('num_patches')
111 def checkNumPatches(cls, v):
112 """Check if the number of patches is valid."""
113 if v <= 0:
114 raise ValueError("Number of patches must be greater than zero.")
115 return v
117 @validator('spatial_unwrapping_method')
118 def checkUnwMethod(cls, v):
119 """Check if unwrapping_method is valid."""
120 if (v != "ilp") & (v != "puma"):
121 raise ValueError("Unwrapping method must be either 'ilp' or 'puma'.")
122 return v
124 @validator('logging_level')
125 def checkLoggingLevel(cls, v):
126 """Check if the logging level is valid."""
127 if v == "":
128 raise ValueError("Empty string is not allowed.")
129 v = v.upper()
130 if v not in ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"]:
131 raise ValueError("Logging level must be one of ('CRITICAL', 'ERROR', "
132 "'WARNING', 'INFO', 'DEBUG', 'NOTSET').")
133 return v
136class PhaseLinking(BaseModel, extra=Extra.forbid):
137 """Template for settings in config file."""
139 use_phase_linking_results: bool = Field(
140 title="Use phase linking results.",
141 description="Use pixels selected in phase linking.",
142 default=False
143 )
145 inverted_path: str = Field(
146 title="The path to the phase linking inverted data directory.",
147 description="Set the path of the inverted data directory.",
148 default="inverted/"
149 )
151 num_siblings: int = Field(
152 title="Sibling threshold.",
153 description="Threshold on the number of siblings applied during phase linking to distinguish PS from DS"
154 "candidates.",
155 default=20
156 )
158 mask_phase_linking_file: Optional[str] = Field(
159 title="Path to spatial mask file for phase linking results.",
160 description="Path to the mask file, e.g. created by sarvey_mask.",
161 default=""
162 )
164 use_ps: bool = Field(
165 title="Use point-like scatterers.",
166 description="Use point-like scatterers (pixels with a low number of siblings) selected in phase linking."
167 "Is applied, only if 'use_phase_linking_results' is true.",
168 default=False
169 )
171 mask_ps_file: str = Field(
172 title="The path to the mask file for ps pixels from phase linking.",
173 description="Set the path of the 'maskPS.h5' file (optional).",
174 default="maskPS.h5"
175 )
177 @validator('inverted_path')
178 def checkPathInverted(cls, v, values):
179 """Check if the inverted path exists."""
180 if values["use_phase_linking_results"]:
181 if v == "":
182 raise ValueError("Empty string is not allowed.")
183 if not os.path.exists(os.path.abspath(v)):
184 raise ValueError(f"inverted_path is invalid: {os.path.abspath(v)}")
185 if not os.path.exists(os.path.join(os.path.abspath(v), "phase_series.h5")):
186 raise ValueError(f"'phase_series.h5' does not exist: {v}")
187 return v
189 @validator('num_siblings')
190 def checkNumSiblings(cls, v, values):
191 """Check is no_siblings is valid."""
192 if not values["use_phase_linking_results"]:
193 if v < 1:
194 raise ValueError("'num_siblings' has to be greater than 0.")
195 return v
197 @validator('mask_phase_linking_file')
198 def checkSpatialMaskPath(cls, v, values):
199 """Check if the path is correct."""
200 if values["use_phase_linking_results"]:
201 if v == "" or v is None:
202 return None
203 else:
204 if not os.path.exists(os.path.abspath(v)):
205 raise ValueError(f"mask_phase_linking_file path is invalid: {v}")
206 return v
208 @validator('use_ps')
209 def checkUsePS(cls, v, values):
210 """Check if use_ps will be applied."""
211 if (not values["use_phase_linking_results"]) and v:
212 raise ValueError("'use_ps' will not be applied, because 'phase_linking' is set to False.")
213 return v
215 @validator('mask_ps_file')
216 def checkPathMaskFilePS(cls, v, values):
217 """Check if the mask file exists."""
218 if values["use_phase_linking_results"] and values["use_ps"]:
219 if v == "":
220 raise ValueError("Empty string is not allowed.")
221 if not os.path.exists(os.path.abspath(v)):
222 raise ValueError(f"mask_ps_file is invalid: {os.path.abspath(v)}")
223 return v
226class Preparation(BaseModel, extra=Extra.forbid):
227 """Template for settings in config file."""
229 start_date: Optional[str] = Field(
230 title="Start date",
231 description="Format: YYYY-MM-DD.",
232 default=None
233 )
235 end_date: Optional[str] = Field(
236 title="End date",
237 description="Format: YYYY-MM-DD.",
238 default=None
239 )
241 ifg_network_type: str = Field(
242 title="Interferogram network type.",
243 description="Set the intererogram network type: 'sb' (small baseline), 'stb' (small temporal baseline), "
244 "'stb_year' (small temporal baseline and yearly ifgs), 'delaunay' (delaunay network), "
245 "or 'star' (single-reference).",
246 default="sb"
247 )
249 num_ifgs: Optional[int] = Field(
250 title="Number of interferograms",
251 description="Set the number of interferograms per image. Might be violated .",
252 default=3
253 )
255 max_tbase: Optional[int] = Field(
256 title="Maximum temporal baseline [days]",
257 description="Set the maximum temporal baseline for the ifg network. (required for: 'sb')",
258 default=100
259 )
261 filter_window_size: int = Field(
262 title="Size of filtering window [pixel]",
263 description="Set the size of window for lowpass filtering.",
264 default=9
265 )
267 @validator('start_date', 'end_date')
268 def checkDates(cls, v):
269 """Check if date format is valid."""
270 if v == "":
271 v = None
273 if v is not None:
274 try:
275 date.fromisoformat(v)
276 except Exception as e:
277 raise ValueError(f"Date needs to be in format: YYYY-MM-DD. {e}")
278 return v
280 @validator('ifg_network_type')
281 def checkNetworkType(cls, v):
282 """Check if the ifg network type is valid."""
283 if (v != "sb") and (v != "star") and (v != "delaunay") and (v != "stb") and (v != "stb_year"):
284 raise ValueError("Interferogram network type has to be 'sb', 'stb', Ästb_year', 'delaunay' or 'star'.")
285 return v
287 @validator('num_ifgs')
288 def checkNumIfgs(cls, v):
289 """Check if the number of ifgs is valid."""
290 if v is not None:
291 if v <= 0:
292 raise ValueError("Number of ifgs must be greater than zero.")
293 return v
295 @validator('max_tbase')
296 def checkMaxTBase(cls, v):
297 """Check if the value for maximum time baseline is valid."""
298 if v is not None:
299 if v <= 0:
300 raise ValueError("Maximum baseline must be greater than zero.")
301 return v
303 @validator('filter_window_size')
304 def checkFilterWdwSize(cls, v):
305 """Check if the filter window size is valid."""
306 if v <= 0:
307 raise ValueError("Filter window size must be greater than zero.")
308 return v
311class ConsistencyCheck(BaseModel, extra=Extra.forbid):
312 """Template for settings in config file."""
314 coherence_p1: float = Field(
315 title="Temporal coherence threshold for first-order points",
316 description="Set the temporal coherence threshold of first-order points for the consistency check.",
317 default=0.9
318 )
320 grid_size: int = Field(
321 title="Grid size [m]",
322 description="Set the grid size in [m] for the consistency check. No grid is applied if 'grid_size' is Zero.",
323 default=200
324 )
326 mask_p1_file: Optional[str] = Field(
327 title="Path to mask file for first-order points",
328 description="Set the path to the mask file in .h5 format.",
329 default=""
330 )
332 num_nearest_neighbours: int = Field(
333 title="Number of nearest neighbours",
334 description="Set number of nearest neighbours for creating arcs.",
335 default=30
336 )
338 max_arc_length: Optional[int] = Field(
339 title="Maximum length of arcs [m]",
340 description="Set the maximum length of arcs.",
341 default=None
342 )
344 velocity_bound: float = Field(
345 title="Bounds on mean velocity for temporal unwrapping [m/year]",
346 description="Set the bound (symmetric) for the mean velocity estimation in temporal unwrapping.",
347 default=0.1
348 )
350 dem_error_bound: float = Field(
351 title="Bounds on DEM error for temporal unwrapping [m]",
352 description="Set the bound (symmetric) for the DEM error estimation in temporal unwrapping.",
353 default=100.0
354 )
356 num_optimization_samples: int = Field(
357 title="Number of samples in the search space for temporal unwrapping",
358 description="Set the number of samples evaluated along the search space for temporal unwrapping.",
359 default=100
360 )
362 arc_unwrapping_coherence: float = Field(
363 title="Arc unwrapping coherence threshold",
364 description="Set the arc unwrapping coherence threshold for the consistency check.",
365 default=0.6
366 )
368 min_num_arc: int = Field(
369 title="Minimum number of arcs per point",
370 description="Set the minimum number of arcs per point.",
371 default=3
372 )
374 @validator('coherence_p1')
375 def checkCoherenceP1(cls, v):
376 """Check if the temporal coherence threshold is valid."""
377 if v < 0:
378 raise ValueError("Temporal coherence threshold cannot be negative.")
379 if v > 1:
380 raise ValueError("Temporal coherence threshold cannot be greater than 1.")
381 return v
383 @validator('grid_size')
384 def checkGridSize(cls, v):
385 """Check if the grid size is valid."""
386 if v < 0:
387 raise ValueError('Grid size cannot be negative.')
388 if v == 0:
389 v = None
390 return v
392 @validator('mask_p1_file')
393 def checkSpatialMaskPath(cls, v):
394 """Check if the path is correct."""
395 if v == "" or v is None:
396 return None
397 else:
398 if not os.path.exists(os.path.abspath(v)):
399 raise ValueError(f"mask_p1_file path is invalid: {v}")
400 return v
402 @validator('num_nearest_neighbours')
403 def checkKNN(cls, v):
404 """Check if the k-nearest neighbours is valid."""
405 if v <= 0:
406 raise ValueError('Number of nearest neighbours cannot be negative or zero.')
407 return v
409 @validator('max_arc_length')
410 def checkMaxArcLength(cls, v):
411 """Check if the maximum length of arcs is valid."""
412 if v is None:
413 return 999999
414 if v <= 0:
415 raise ValueError('Maximum arc length must be positive.')
416 return v
418 @validator('velocity_bound')
419 def checkVelocityBound(cls, v):
420 """Check if the velocity bound is valid."""
421 if v <= 0:
422 raise ValueError('Velocity bound cannot be negative or zero.')
423 return v
425 @validator('dem_error_bound')
426 def checkDEMErrorBound(cls, v):
427 """Check if the DEM error bound is valid."""
428 if v <= 0:
429 raise ValueError('DEM error bound cannot be negative or zero.')
430 return v
432 @validator('num_optimization_samples')
433 def checkNumSamples(cls, v):
434 """Check if the number of samples for the search space is valid."""
435 if v <= 0:
436 raise ValueError('Number of optimization samples cannot be negative or zero.')
437 return v
439 @validator('arc_unwrapping_coherence')
440 def checkArcCoherence(cls, v):
441 """Check if the arc coherence threshold is valid."""
442 if v < 0:
443 raise ValueError('Arc unwrapping coherence threshold cannot be negativ.')
444 if v > 1:
445 raise ValueError('Arc unwrapping coherence threshold cannot be greater than 1.')
446 return v
448 @validator('min_num_arc')
449 def checkMinNumArc(cls, v):
450 """Check if the minimum number of arcs is valid."""
451 if v < 0:
452 raise ValueError('Velocity bound cannot be negative.')
453 return v
456class Unwrapping(BaseModel, extra=Extra.forbid):
457 """Template for settings in config file."""
459 use_arcs_from_temporal_unwrapping: bool = Field(
460 title="Use arcs from temporal unwrapping",
461 description="If true, use same arcs from temporal unwrapping, where bad arcs were already removed."
462 "If false, apply new delaunay triangulation.",
463 default=True
464 )
467class Filtering(BaseModel, extra=Extra.forbid):
468 """Template for filtering settings in config file."""
470 coherence_p2: float = Field(
471 title="Temporal coherence threshold",
472 description="Set the temporal coherence threshold for the filtering step.",
473 default=0.8
474 )
476 apply_aps_filtering: bool = Field(
477 title="Apply atmosphere filtering",
478 description="Set whether to filter atmosphere or to skip it.",
479 default=True
480 )
482 interpolation_method: str = Field(
483 title="Spatial interpolation method.",
484 description="Method for interpolating atmosphere in space ('linear', 'cubic' or 'kriging').",
485 default="kriging"
486 )
488 grid_size: int = Field(
489 title="Grid size [m].",
490 description="Set the grid size for spatial filtering.",
491 default=1000
492 )
494 mask_p2_file: Optional[str] = Field(
495 title="Path to spatial mask file for second-order points.",
496 description="Path to the mask file, e.g. created by sarvey_mask.",
497 default=""
498 )
500 use_moving_points: bool = Field(
501 title="Use moving points",
502 description="Set whether to use moving points in the filtering step.",
503 default=True
504 )
506 max_temporal_autocorrelation: float = Field(
507 title="Max temporal auto correlation.",
508 description="Set temporal autocorrelation threshold for the selection of stable/linearly moving points.",
509 default=0.3
510 )
512 @validator('coherence_p2')
513 def checkTempCohThrsh2(cls, v):
514 """Check if the temporal coherence threshold is valid."""
515 if v < 0:
516 raise ValueError("Temporal coherence threshold cannot be negative.")
517 if v > 1:
518 raise ValueError("Temporal coherence threshold cannot be greater than 1.")
519 return v
521 @validator('interpolation_method')
522 def checkInterpolationMethod(cls, v):
523 """Check if the interpolation method is valid."""
524 if (v.lower() != "linear") and (v.lower() != "cubic") and (v.lower() != "kriging"):
525 raise ValueError("Method for interpolating atmosphere in space needs to be either 'linear', 'cubic' "
526 "or 'kriging'.")
527 return v
529 @validator('grid_size')
530 def checkGridSize(cls, v):
531 """Check if the grid size is valid."""
532 if v < 0:
533 raise ValueError("Grid size cannot be negative.")
534 else:
535 return v
537 @validator('mask_p2_file')
538 def checkSpatialMaskPath(cls, v):
539 """Check if the path is correct."""
540 if v == "" or v is None:
541 return None
542 else:
543 if not os.path.exists(os.path.abspath(v)):
544 raise ValueError(f"mask_p2_file path is invalid: {v}")
545 return v
547 @validator('max_temporal_autocorrelation')
548 def checkMaxAutoCorr(cls, v):
549 """Check if the value is correct."""
550 if v < 0 or v > 1:
551 raise ValueError(f"max_temporal_autocorrelation has to be between 0 and 1: {v}")
552 return v
555class Densification(BaseModel, extra=Extra.forbid):
556 """Template for densification settings in config file."""
558 num_connections_to_p1: int = Field(
559 title="Number of connections in temporal unwrapping.",
560 description="Set number of connections between second-order point and closest first-order points for temporal "
561 "unwrapping.",
562 default=5
563 )
565 max_distance_to_p1: int = Field(
566 title="Maximum distance to nearest first-order point [m]",
567 description="Set threshold on the distance between first-order points and to be temporally unwrapped"
568 "second-order point.",
569 default=2000
570 )
572 velocity_bound: float = Field(
573 title="Bounds on mean velocity for temporal unwrapping [m/year]",
574 description="Set the bound (symmetric) for the mean velocity in temporal unwrapping.",
575 default=0.15
576 )
578 dem_error_bound: float = Field(
579 title="Bounds on DEM error for temporal unwrapping [m]",
580 description="Set the bound (symmetric) for the DEM error estimation in temporal unwrapping.",
581 default=100.0
582 )
584 num_optimization_samples: int = Field(
585 title="Number of samples in the search space for temporal unwrapping",
586 description="Set the number of samples evaluated along the search space for temporal unwrapping.",
587 default=100
588 )
590 arc_unwrapping_coherence: float = Field(
591 title="Arc unwrapping coherence threshold for densification",
592 description="Set arc unwrapping coherence threshold for densification.",
593 default=0.5
594 )
596 @validator('num_connections_to_p1')
597 def checkNumConn1(cls, v):
598 """Check if num_connections_p1 are valid."""
599 if v <= 0:
600 raise ValueError(f"num_connections_p1 must be greater than 0: {v}")
601 return v
603 @validator('max_distance_to_p1')
604 def checkMaxDistanceP1(cls, v):
605 """Check if the maximum distance to nearest first-order points is valid."""
606 if v < 0:
607 raise ValueError('Maximum distance to first-order points cannot be negative.')
608 return v
610 @validator('velocity_bound')
611 def checkVelocityBound(cls, v):
612 """Check if the velocity bound is valid."""
613 if v <= 0:
614 raise ValueError('Velocity bound cannot be negative or zero.')
615 return v
617 @validator('dem_error_bound')
618 def checkDEMErrorBound(cls, v):
619 """Check if the DEM error bound is valid."""
620 if v <= 0:
621 raise ValueError('DEM error bound cannot be negative or zero.')
622 return v
624 @validator('num_optimization_samples')
625 def checkNumSamples(cls, v):
626 """Check if the number of samples for the search space is valid."""
627 if v <= 0:
628 raise ValueError('Number of optimization samples cannot be negative or zero.')
629 return v
631 @validator('arc_unwrapping_coherence')
632 def checkCoherenceThresh(cls, v):
633 """Check if arc_unwrapping_coherence is valid."""
634 if v < 0 or v > 1:
635 raise ValueError(f"coherence_threshold is not between 0 and 1: {v}")
636 return v
639class Config(BaseModel):
640 """Configuration for sarvey."""
642 # title has to be the name of the class. Needed for creating default file
643 general: General = Field(
644 title="General", description=""
645 )
647 phase_linking: PhaseLinking = Field(
648 title="PhaseLinking", description=""
649 )
651 preparation: Preparation = Field(
652 title="Preparation", description=""
653 )
655 consistency_check: ConsistencyCheck = Field(
656 title="ConsistencyCheck", description=""
657 )
659 unwrapping: Unwrapping = Field(
660 title="Unwrapping", description=""
661 )
663 filtering: Filtering = Field(
664 title="Filtering", description=""
665 )
667 densification: Densification = Field(
668 title="Densification", description=""
669 )
672def loadConfiguration(*, path: str) -> dict:
673 """Load configuration json file.
675 Parameters
676 ----------
677 path : str
678 Path to the configuration json file.
680 Returns
681 -------
682 : Config
683 An object of class Config
685 Raises
686 ------
687 JSONDecodeError
688 If failed to parse the json file to the dictionary.
689 FileNotFoundError
690 Config file not found.
691 IOError
692 Invalid JSON file.
693 ValueError
694 Invalid value for configuration object.
695 """
696 try:
697 with open(path) as config_fp:
698 config = json5.load(config_fp)
699 config = Config(**config)
700 except JSONDecodeError as e:
701 raise IOError(f'Failed to load the configuration json file => {e}')
702 return config