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

dbekaert / RAiDER / 85e7e6a1-26d6-4ec1-9cfc-dbb792a60c5f

pending completion
85e7e6a1-26d6-4ec1-9cfc-dbb792a60c5f

Pull #426

circleci

GitHub
Release: v0.2.0 (#424)
Pull Request #426: Pulling refs/tags/v0.2.0 into dev

4793 of 4793 new or added lines in 36 files covered. (100.0%)

1942 of 5098 relevant lines covered (38.09%)

0.38 hits per line

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

58.43
/tools/RAiDER/cli/validators.py
1
from argparse import Action, ArgumentError, ArgumentTypeError
1✔
2

3
import importlib
1✔
4
import itertools
1✔
5
import os
1✔
6
import re
1✔
7

8
import pandas as pd
1✔
9
import numpy as np
1✔
10

11
from datetime import time, timedelta, datetime, date
1✔
12
from textwrap import dedent
1✔
13
from time import strptime
1✔
14

15
from RAiDER.llreader import BoundingBox, Geocube, RasterRDR, StationFile, GeocodedFile, Geocube
1✔
16
from RAiDER.losreader import Zenith, Conventional, Raytracing
1✔
17
from RAiDER.utilFcns import rio_extents, rio_profile
1✔
18

19
_BUFFER_SIZE = 0.2 # default buffer size in lat/lon degrees
1✔
20

21
def enforce_wm(value):
1✔
22
    model = value.upper().replace("-", "")
1✔
23
    try:
1✔
24
        _, model_obj = modelName2Module(model)
1✔
25
    except ModuleNotFoundError:
×
26
        raise NotImplementedError(
×
27
            dedent('''
28
                Model {} is not yet fully implemented,
29
                please contribute!
30
                '''.format(model))
31
        )
32
    return model_obj()
1✔
33

34

35
def get_los(args):
1✔
36
    if ('orbit_file' in args.keys()) and (args['orbit_file'] is not None):
1✔
37
        if args.ray_trace:
1✔
38
            los = Raytracing(args.orbit_file)
1✔
39
        else:
40
            los = Conventional(args.orbit_file)
×
41
    elif ('los_file' in args.keys()) and (args['los_file'] is not None):
1✔
42
        if args.ray_trace:
×
43
            los = Raytracing(args.los_file, args.los_convention)
×
44
        else:
45
            los = Conventional(args.los_file, args.los_convention)
×
46
    elif ('los_cube' in args.keys()) and (args['los_cube'] is not None):
1✔
47
        raise NotImplementedError('LOS_cube is not yet implemented')
×
48
#        if args.ray_trace:
49
#            los = Raytracing(args.los_cube)
50
#        else:
51
#            los = Conventional(args.los_cube)
52
    else:
53
        los = Zenith()
1✔
54

55
    return los
1✔
56

57

58
def get_heights(args, out, station_file, bounding_box=None):
1✔
59
    '''
60
    Parse the Height info and download a DEM if needed
61
    '''
62
    dem_path = os.path.join(out, 'geom')
1✔
63
    if not os.path.exists(dem_path):
1✔
64
        os.mkdir(dem_path)
1✔
65
    out = {
1✔
66
            'dem': None,
67
            'height_file_rdr': None,
68
            'height_levels': None,
69
        }
70

71
    if 'dem' in args.keys():
1✔
72
        if (station_file is not None):
×
73
            if 'Hgt_m' not in pd.read_csv(station_file):
×
74
                out['dem'] = os.path.join(dem_path, 'GLO30.dem')
×
75
        elif os.path.exists(args.dem):
×
76
            out['dem'] = args['dem']
×
77
            if bounding_box is not None:
×
78
                dem_bounds = rio_extents(rio_profile(args.dem))
×
79
                lats = dem_bounds[:2]
×
80
                lons = dem_bounds[2:]
×
81
                if isOutside(
×
82
                    bounding_box,
83
                    getBufferedExtent(
84
                        lats,
85
                        lons,
86
                        buf=_BUFFER_SIZE,
87
                    )
88
                ):
89
                    raise ValueError(
×
90
                                'Existing DEM does not cover the area of the input lat/lon '
91
                                'points; either move the DEM, delete it, or change the input '
92
                                'points.'
93
                            )
94
        else:
95
            pass # will download the dem later
×
96

97
    elif 'height_file_rdr' in args.keys():
1✔
98
        out['height_file_rdr'] = args.height_file_rdr
×
99

100
    elif 'height_levels' in args.keys():
1✔
101
        l = re.findall('[0-9]+', args.height_levels)
1✔
102
        out['height_levels'] = [float(ll) for ll in l]
1✔
103

104
    else:
105
        # download the DEM if needed
106
        out['dem'] = os.path.join(dem_path, 'GLO30.dem')
×
107

108
    return out
1✔
109

110

111
def get_query_region(args):
1✔
112
    '''
113
    Parse the query region from inputs
114
    '''
115
    # Get bounds from the inputs
116
    # make sure this is first
117
    if ('use_dem_latlon' in args.keys()) and args['use_dem_latlon']:
1✔
118
        query = GeocodedFile(args.dem, is_dem=True)
×
119

120
    elif 'lat_file' in args.keys():
1✔
121
        hgt_file = args.get('hgt_file_rdr', None) # only get it if exists
×
122
        query    = RasterRDR(args.lat_file, args.lon_file, hgt_file)
×
123

124
    elif 'station_file' in args.keys():
1✔
125
        query = StationFile(args.station_file)
×
126

127
    elif 'bounding_box' in args.keys():
1✔
128
        bbox = enforce_bbox(args.bounding_box)
1✔
129
        if (np.min(bbox[0]) < -90) | (np.max(bbox[1]) > 90):
1✔
130
            raise ValueError('Lats are out of N/S bounds; are your lat/lon coordinates switched? Should be SNWE')
×
131
        query = BoundingBox(bbox)
1✔
132

133
    elif 'geocoded_file' in args.keys():
×
134
        query = GeocodedFile(args.geocoded_file, is_dem=False)
×
135

136
    ## untested
137
    elif 'los_cube' in args.keys():
×
138
        query = Geocube(args.los_cube)
×
139

140
    else:
141
        # TODO: Need to incorporate the cube
142
        raise ValueError('No valid query points or bounding box found in the configuration file')
×
143

144

145
    return query
1✔
146

147

148
def enforce_bbox(bbox):
1✔
149
    """
150
    Enforce a valid bounding box
151
    """
152
    bbox = [float(d) for d in bbox.strip().split()]
1✔
153

154
    # Check the bbox
155
    if len(bbox) != 4:
1✔
156
        raise ValueError("bounding box must have 4 elements!")
×
157
    S, N, W, E = bbox
1✔
158

159
    if N <= S or E <= W:
1✔
160
        raise ValueError('Bounding box has no size; make sure you use "S N W E"')
1✔
161

162
    for sn in (S, N):
1✔
163
        if sn < -90 or sn > 90:
1✔
164
            raise ValueError('Lats are out of S/N bounds (-90 to 90).')
1✔
165

166
    for we in (W, E):
1✔
167
        if we < -180 or we > 180:
1✔
168
            raise ValueError('Lons are out of W/E bounds (-180 to 180); Lons in the format of (0 to 360) are not supported.')
1✔
169

170
    return bbox
1✔
171

172

173
def parse_dates(arg_dict):
1✔
174
    '''
175
    Determine the requested dates from the input parameters
176
    '''
177

178
    if 'date_list' in arg_dict.keys():
1✔
179
        l = arg_dict['date_list']
1✔
180
        if isinstance(l, str):
1✔
181
            l = re.findall('[0-9]+', l)
×
182
        L = [enforce_valid_dates(d) for d in l]
1✔
183

184
    else:
185
        try:
1✔
186
            start = arg_dict['date_start']
1✔
187
        except KeyError:
×
188
            raise ValueError('Inputs must include either date_list or date_start')
×
189
        start = enforce_valid_dates(start)
1✔
190

191
        if 'date_end' in arg_dict.keys():
1✔
192
            end = arg_dict['date_end']
1✔
193
            end = enforce_valid_dates(end)
1✔
194
        else:
195
           end = start
1✔
196

197
        if 'date_step' in arg_dict.keys():
1✔
198
            step = int(arg_dict['date_step'])
1✔
199
        else:
200
            step = 1
1✔
201

202
        L = [start + timedelta(days=step) for step in range(0, (end - start).days + 1, step)]
1✔
203

204
    return L
1✔
205

206

207
def enforce_valid_dates(arg):
1✔
208
    """
209
    Parse a date from a string in pseudo-ISO 8601 format.
210
    """
211
    year_formats = (
1✔
212
        '%Y-%m-%d',
213
        '%Y%m%d',
214
        '%d',
215
        '%j',
216
    )
217

218
    for yf in year_formats:
1✔
219
        try:
1✔
220
            return datetime.strptime(str(arg), yf)
1✔
221
        except ValueError:
1✔
222
            pass
1✔
223

224

225
    raise ValueError(
1✔
226
        'Unable to coerce {} to a date. Try %Y-%m-%d'.format(arg)
227
    )
228

229

230
def enforce_time(arg_dict):
1✔
231
    '''
232
    Parse an input time (required to be ISO 8601)
233
    '''
234
    try:
1✔
235
        arg_dict['time'] = convert_time(arg_dict['time'])
1✔
236
    except KeyError:
×
237
        raise ValueError('You must specify a "time" in the input config file')
×
238

239
    if 'end_time' in arg_dict.keys():
1✔
240
        arg_dict['end_time'] = convert_time(arg_dict['end_time'])
×
241
    return arg_dict
1✔
242

243

244
def convert_time(inp):
1✔
245
    time_formats = (
1✔
246
        '',
247
        'T%H:%M:%S.%f',
248
        'T%H%M%S.%f',
249
        '%H%M%S.%f',
250
        'T%H:%M:%S',
251
        '%H:%M:%S',
252
        'T%H%M%S',
253
        '%H%M%S',
254
        'T%H:%M',
255
        'T%H%M',
256
        '%H:%M',
257
        'T%H',
258
    )
259
    timezone_formats = (
1✔
260
        '',
261
        'Z',
262
        '%z',
263
    )
264
    all_formats = map(
1✔
265
        ''.join,
266
        itertools.product(time_formats, timezone_formats)
267
    )
268

269
    for tf in all_formats:
1✔
270
        try:
1✔
271
            return time(*strptime(inp, tf)[3:6])
1✔
272
        except ValueError:
1✔
273
            pass
1✔
274

275
    raise ValueError(
1✔
276
                'Unable to coerce {} to a time.'+
277
                'Try T%H:%M:%S'.format(inp)
278
        )
279

280

281
def modelName2Module(model_name):
1✔
282
    """Turn an arbitrary string into a module name.
283
    Takes as input a model name, which hopefully looks like ERA-I, and
284
    converts it to a module name, which will look like erai. I doesn't
285
    always produce a valid module name, but that's not the goal. The
286
    goal is just to handle common cases.
287
    Inputs:
288
       model_name  - Name of an allowed weather model (e.g., 'era-5')
289
    Outputs:
290
       module_name - Name of the module
291
       wmObject    - callable, weather model object
292
    """
293
    module_name = 'RAiDER.models.' + model_name.lower().replace('-', '')
1✔
294
    model_module = importlib.import_module(module_name)
1✔
295
    wmObject = getattr(model_module, model_name.upper().replace('-', ''))
1✔
296
    return module_name, wmObject
1✔
297

298

299
def getBufferedExtent(lats, lons=None, buf=0.):
1✔
300
    '''
301
    get the bounding box around a set of lats/lons
302
    '''
303
    if lons is None:
1✔
304
        lats, lons = lats[..., 0], lons[..., 1]
×
305

306
    try:
1✔
307
        if (lats.size == 1) & (lons.size == 1):
1✔
308
            out = [lats - buf, lats + buf, lons - buf, lons + buf]
×
309
        elif (lats.size > 1) & (lons.size > 1):
1✔
310
            out = [np.nanmin(lats), np.nanmax(lats), np.nanmin(lons), np.nanmax(lons)]
1✔
311
        elif lats.size == 1:
×
312
            out = [lats - buf, lats + buf, np.nanmin(lons), np.nanmax(lons)]
×
313
        elif lons.size == 1:
×
314
            out = [np.nanmin(lats), np.nanmax(lats), lons - buf, lons + buf]
×
315
    except AttributeError:
1✔
316
        if (isinstance(lats, tuple) or isinstance(lats, list)) and len(lats) == 2:
1✔
317
            out = [min(lats) - buf, max(lats) + buf, min(lons) - buf, max(lons) + buf]
1✔
318
    except Exception as e:
×
319
        raise RuntimeError('Not a valid lat/lon shape or variable')
×
320

321
    return np.array(out)
1✔
322

323

324
def isOutside(extent1, extent2):
1✔
325
    '''
326
    Determine whether any of extent1  lies outside extent2
327
    extent1/2 should be a list containing [lower_lat, upper_lat, left_lon, right_lon]
328
    Equal extents are considered "inside"
329
    '''
330
    t1 = extent1[0] < extent2[0]
1✔
331
    t2 = extent1[1] > extent2[1]
1✔
332
    t3 = extent1[2] < extent2[2]
1✔
333
    t4 = extent1[3] > extent2[3]
1✔
334
    if np.any([t1, t2, t3, t4]):
1✔
335
        return True
1✔
336
    return False
1✔
337

338

339
def isInside(extent1, extent2):
1✔
340
    '''
341
    Determine whether all of extent1 lies inside extent2
342
    extent1/2 should be a list containing [lower_lat, upper_lat, left_lon, right_lon].
343
    Equal extents are considered "inside"
344
    '''
345
    t1 = extent1[0] <= extent2[0]
1✔
346
    t2 = extent1[1] >= extent2[1]
1✔
347
    t3 = extent1[2] <= extent2[2]
1✔
348
    t4 = extent1[3] >= extent2[3]
1✔
349
    if np.all([t1, t2, t3, t4]):
1✔
350
        return True
1✔
351
    return False
1✔
352

353

354
## below are for downloadGNSSDelays
355
def date_type(arg):
1✔
356
    """
357
    Parse a date from a string in pseudo-ISO 8601 format.
358
    """
359
    year_formats = (
×
360
        '%Y-%m-%d',
361
        '%Y%m%d',
362
        '%d',
363
        '%j',
364
    )
365

366
    for yf in year_formats:
×
367
        try:
×
368
            return date(*strptime(arg, yf)[0:3])
×
369
        except ValueError:
×
370
            pass
×
371

372
    raise ArgumentTypeError(
×
373
        'Unable to coerce {} to a date. Try %Y-%m-%d'.format(arg)
374
    )
375

376

377
class MappingType(object):
1✔
378
    """
379
    A type that maps arguments to constants.
380

381
    # Example
382
    ```
383
    mapping = MappingType(foo=42, bar="baz").default(None)
384
    assert mapping("foo") == 42
385
    assert mapping("bar") == "baz"
386
    assert mapping("hello") is None
387
    ```
388
    """
389
    UNSET = object()
1✔
390

391
    def __init__(self, **kwargs):
1✔
392
        self.mapping = kwargs
×
393
        self._default = self.UNSET
×
394

395
    def default(self, default):
1✔
396
        """Set a default value if no mapping is found"""
397
        self._default = default
×
398
        return self
×
399

400
    def __call__(self, arg):
1✔
401
        if arg in self.mapping:
×
402
            return self.mapping[arg]
×
403

404
        if self._default is self.UNSET:
×
405
            raise KeyError(
×
406
                "Invalid choice '{}', must be one of {}".format(
407
                    arg, list(self.mapping.keys())
408
                )
409
            )
410

411
        return self._default
×
412

413

414
class IntegerType(object):
1✔
415
    """
416
    A type that converts arguments to integers.
417

418
    # Example
419
    ```
420
    integer = IntegerType(0, 100)
421
    assert integer("0") == 0
422
    assert integer("100") == 100
423
    integer("-10")  # Raises exception
424
    ```
425
    """
426

427
    def __init__(self, lo=None, hi=None):
1✔
428
        self.lo = lo
×
429
        self.hi = hi
×
430

431
    def __call__(self, arg):
1✔
432
        integer = int(arg)
×
433

434
        if self.lo is not None and integer < self.lo:
×
435
            raise ArgumentTypeError("Must be greater than {}".format(self.lo))
×
436
        if self.hi is not None and integer > self.hi:
×
437
            raise ArgumentTypeError("Must be less than {}".format(self.hi))
×
438

439
        return integer
×
440

441

442
class IntegerMappingType(MappingType, IntegerType):
1✔
443
    """
444
    An integer type that converts non-integer types through a mapping.
445

446
    # Example
447
    ```
448
    integer = IntegerMappingType(0, 100, random=42)
449
    assert integer("0") == 0
450
    assert integer("100") == 100
451
    assert integer("random") == 42
452
    ```
453
    """
454

455
    def __init__(self, lo=None, hi=None, mapping={}, **kwargs):
1✔
456
        IntegerType.__init__(self, lo, hi)
×
457
        kwargs.update(mapping)
×
458
        MappingType.__init__(self, **kwargs)
×
459

460
    def __call__(self, arg):
1✔
461
        try:
×
462
            return IntegerType.__call__(self, arg)
×
463
        except ValueError:
×
464
            return MappingType.__call__(self, arg)
×
465

466

467
class DateListAction(Action):
1✔
468
    """An Action that parses and stores a list of dates"""
469

470
    def __init__(
1✔
471
        self,
472
        option_strings,
473
        dest,
474
        nargs=None,
475
        const=None,
476
        default=None,
477
        type=None,
478
        choices=None,
479
        required=False,
480
        help=None,
481
        metavar=None
482
    ):
483
        if type is not date_type:
×
484
            raise ValueError("type must be `date_type`!")
×
485

486
        super().__init__(
×
487
            option_strings=option_strings,
488
            dest=dest,
489
            nargs=nargs,
490
            const=const,
491
            default=default,
492
            type=type,
493
            choices=choices,
494
            required=required,
495
            help=help,
496
            metavar=metavar
497
        )
498

499
    def __call__(self, parser, namespace, values, option_string=None):
1✔
500
        if len(values) > 3 or not values:
×
501
            raise ArgumentError(self, "Only 1, 2 dates, or 2 dates and interval may be supplied")
×
502

503
        if len(values) == 2:
×
504
            start, end = values
×
505
            values = [start + timedelta(days=k) for k in range(0, (end - start).days + 1, 1)]
×
506
        elif len(values) == 3:
×
507
            start, end, stepsize = values
×
508

509
            if not isinstance(stepsize.day, int):
×
510
                raise ArgumentError(self, "The stepsize should be in integer days")
×
511

512
            new_year = date(year=stepsize.year, month=1, day=1)
×
513
            stepsize = (stepsize - new_year).days + 1
×
514

515
            values = [start + timedelta(days=k)
×
516
                      for k in range(0, (end - start).days + 1, stepsize)]
517

518
        setattr(namespace, self.dest, values)
×
519

520

521
class BBoxAction(Action):
1✔
522
    """An Action that parses and stores a valid bounding box"""
523

524
    def __init__(
1✔
525
        self,
526
        option_strings,
527
        dest,
528
        nargs=None,
529
        const=None,
530
        default=None,
531
        type=None,
532
        choices=None,
533
        required=False,
534
        help=None,
535
        metavar=None
536
    ):
537
        if nargs != 4:
×
538
            raise ValueError("nargs must be 4!")
×
539

540
        super().__init__(
×
541
            option_strings=option_strings,
542
            dest=dest,
543
            nargs=nargs,
544
            const=const,
545
            default=default,
546
            type=type,
547
            choices=choices,
548
            required=required,
549
            help=help,
550
            metavar=metavar
551
        )
552

553
    def __call__(self, parser, namespace, values, option_string=None):
1✔
554
        S, N, W, E = values
×
555

556
        if N <= S or E <= W:
×
557
            raise ArgumentError(self, 'Bounding box has no size; make sure you use "S N W E"')
×
558

559
        for sn in (S, N):
×
560
            if sn < -90 or sn > 90:
×
561
                raise ArgumentError(self, 'Lats are out of S/N bounds (-90 to 90).')
×
562

563
        for we in (W, E):
×
564
            if we < -180 or we > 180:
×
565
                raise ArgumentError(self, 'Lons are out of W/E bounds (-180 to 180); Lons in the format of (0 to 360) are not supported.')
×
566

567
        setattr(namespace, self.dest, values)
×
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

© 2026 Coveralls, Inc