def propanglesEngine(config): ''' Calculate the propagation angles and distances between elements and a set of reflectors. ''' psec = 'propangles' msec = 'measurement' try: # Grab the reflector positions rflfile = config.get(psec, 'reflectors') except Exception as e: err = 'Configuration must specify reflectors in [%s]' % psec raise HabisConfigError.fromException(err, e) try: # Grab the element positions by facet eltfiles = config.getlist(psec, 'elements') if len(eltfiles) < 1: err = 'Key elements must contain at least one entry' raise HabisConfigError(err) except Exception as e: err = 'Configuration must specify elements in [%s]' % psec raise HabisConfigError.fromException(err, e) try: # Determine whether to use surface or center distances useradius = not config.get(psec, 'centers', mapper=bool, default=True) except Exception as e: err = 'Invalid specification of optional centers in [%s]' % psec raise HabisConfigError.fromException(err, e) if useradius: # Grab the reflector radius if necessary try: radius = config.get(msec, 'radius', mapper=float) except Exception as e: err = 'Configuration must specify radius in [%s]' % msec raise HabisConfigError.fromException(err, e) try: # Grab the output distance file distfile = config.get(psec, 'distances') except Exception as e: err = 'Configuration must specify distances in [%s]' % psec raise HabisConfigError.fromException(err, e) try: # Grab the output angle file angfile = config.get(psec, 'angles') except Exception as e: err = 'Configuration must specify angles in [%s]' % psec raise HabisConfigError.fromException(err, e) # Load the element and reflector positions, then compute distances and angles elements = [np.loadtxt(efile) for efile in eltfiles] nedim = elements[0].shape[1] for el in elements[1:]: if el.shape[1] != nedim: raise ValueError('Dimensionality of all element files must agree') # Ignore an optional sound-speed column in the reflector coordinates reflectors = np.loadtxt(rflfile)[:, :nedim] distances, thetas = wavepaths(elements, reflectors) # Concatenate the distance and angle lists distances = np.concatenate(distances, axis=0) thetas = np.concatenate(thetas, axis=0) # Offset distances by radius, if desired if useradius: distances -= radius # Save the outputs np.savetxt(distfile, distances, fmt='%16.8f') np.savetxt(angfile, thetas, fmt='%16.8f')
def wavesfcEngine(config): ''' Use positions of elements and targets specified in the provided config, combined with a specified sound speed (to override per-reflector sound speeds), to compute control points that define a reflector surface. ''' msection = 'measurement' wsection = 'wavesfc' try: # Try to grab the input and output files eltfiles = matchfiles(config.getlist(wsection, 'elements')) rflfile = config.get(wsection, 'reflectors') outfile = config.get(wsection, 'output') except Exception as e: err = 'Configuration must specify elements, reflectors and output in [%s]' % wsection raise HabisConfigError.fromException(err, e) try: # Try to read the input time files timefiles = matchfiles(config.getlist(wsection, 'timefile')) except Exception as e: err = 'Configuration must specify timefile in [%s]' % wsection raise HabisConfigError.fromException(err, e) try: # Grab the outlier range and group size olrange = config.get(wsection, 'olrange', mapper=float, default=None) olgroup = config.get(wsection, 'olgroup', mapper=int, default=64) except Exception as e: err = 'Invalid specification of optionals outrange or outgroup in [%s]' % wsection raise HabisConfigError.fromException(err, e) try: # Grab the sound speed c = config.get(msection, 'c', mapper=float) rad = config.get(msection, 'radius', mapper=float) except Exception as e: err = 'Configuration must specify c and radius in [%s]' % msection raise HabisConfigError.fromException(err, e) try: # Use the reflector radius for missing arrival-time values usemiss = config.get(wsection, 'usemiss', mapper=bool, default=False) except Exception as e: err = 'Invalid specification of optional usemiss in [%s]' % wsection raise HabisConfigError.fromException(err, e) try: # Use the reflector radius if it is larger than time-imputed radius maxrad = config.get(wsection, 'maxrad', mapper=bool, default=False) except Exception as e: err = 'Invalid specification of optional maxrad in [%s]' % wsection raise HabisConfigError.fromException(err, e) # Read the element positions and backscatter arrival times elements = loadmatlist(eltfiles, nkeys=1) times = { k[0]: v for k, v in loadmatlist(timefiles, scalar=False, nkeys=2).items() if k[0] == k[1] } reflectors = np.loadtxt(rflfile, ndmin=2) nrefl, nrdim = reflectors.shape # Make sure the reflector specification includes speed and radius if nrdim == 3: reflectors = np.concatenate([reflectors, [[c, rad]] * nrefl], axis=1) elif nrdim == 4: reflectors = np.concatenate([reflectors, [[rad]] * nrefl], axis=1) elif nrdim != 5: raise ValueError('Reflector file must contain 3, 4 or 5 columns') # Split the file name and extension fbase, fext = os.path.splitext(outfile) # Add a target specifier to outputs for multiple targets nrefl = len(reflectors) if nrefl == 1: idfmt = '' else: idfmt = '.Target{{0:0{width}d}}'.format(width=cutil.numdigits(nrefl)) for ridx, refl in enumerate(reflectors): pos = refl[:3] lc, rad = refl[3:] # Convert all times to distances pdists = {} for el, elpos in elements.items(): # Ray from reflector center to element dl = elpos - pos # Find distance to element and normalize ray ll = norm(dl) dl /= ll try: # Convert arrival time to distance from center ds = ll - times[el][ridx] * lc / 2.0 except (KeyError, IndexError, TypeError): # Use default radius or skip missing value if usemiss: ds = rad else: continue else: if maxrad: ds = max(rad, ds) # Valid points will be inside segment or opposite reflector if ds <= ll: # Record distance and ray pdists[el] = (ds, dl) if olrange is not None: # Group distances according to olgroup pgrps = defaultdict(dict) for el, (ds, _) in pdists.items(): pgrps[int(el / olgroup)][el] = ds # Filter outliers from each group and flatten map pdists = { el: pdists[el] for pg in pgrps.values() for el in stats.mask_outliers(pg, olrange) } # Sort remaining values, separate indices and control points cpel = sorted(pdists.keys()) cpts = np.array([pos + pdists[el][0] * pdists[el][1] for el in cpel]) fname = fbase + idfmt.format(ridx) + fext # Project control points for tesselation zmin = np.min(cpts[:, -1]) zmax = np.max(cpts[:, -1]) if np.allclose(zmin, zmax): # For flat structures, just use x-y tris = Delaunay(cpts[:, :2]) else: # Otherwise, project radially zdiff = zmax - zmin prctr = np.mean(cpts, axis=0) prctr[-1] = zmax + zdiff prref = np.array(prctr) prref[-1] = zmin l = cpts - prctr[np.newaxis, :] d = (prref - prctr)[-1] / l[:, -1] if np.any(np.isinf(d)) or np.any(np.isnan(d)): raise ValueError('Failure in projection for tesselation') ppts = prctr[np.newaxis, :] + d[:, np.newaxis] * l tris = Delaunay(ppts[:, :2]) # Save the control points and triangles np.savez(fname, nodes=cpts, triangles=tris.simplices, elements=cpel)
def _throw(e, msg, sec=tsec): raise HabisConfigError.fromException(f'{msg} in section [{sec}]', e)
parser.add_argument('slowness', type=str, help='Numpy npy file containing the slowness image') parser.add_argument( 'output', type=str, help='Integral output, keymap or binary matrix (with -c)') args = parser.parse_args(sys.argv[1:]) # Load the tracer configuration try: config = HabisConfigParser(args.tracer) except Exception as e: err = f'Unable to load configuration {args.tracer}' raise HabisConfigError.fromException(err, e) tracer = PathTracer.fromconf(config) # Load the element coordinates and target list elements = loadkeymat(args.elements) if args.trlist: # Load the r-[t] keymap and flatten to a trlist targets = loadkeymat(args.targets) targets = [(t, r) for r, tl in targets.items() for t in tl] else: targets = mio.readbmat(args.targets) s = np.load(args.slowness)
def exdelayEngine(config): ''' Use positions of elements and reflectors specified in the provided config, combined with a specified sound speed and reflector radius, to estimate the round-trip arrival times from every element to the reflector and back. ''' msection = 'measurement' esection = 'exdelays' try: # Try to grab the input and output files eltfiles = config.getlist(esection, 'elements') rflfile = config.get(esection, 'reflectors') timefile = config.get(esection, 'timefile') except Exception as e: err = 'Configuration must specify elements, reflectors and timefile in [%s]' % esection raise HabisConfigError.fromException(err, e) # Grab the sound speed and reflector radius try: c = config.get(msection, 'c', mapper=float) r = config.get(msection, 'radius', mapper=float) except Exception as e: err = 'Configuration must specify c and radius in [%s]' % msection raise HabisConfigError.fromException(err, e) try: # Read an optional global time offset offset = config.get(esection, 'offset', mapper=float, default=0.) except Exception as e: err = 'Invalid optional offset in [%s]' % esection raise HabisConfigError.fromException(err, e) # Read the element and reflector positions eltspos = dict(kp for efile in eltfiles for kp in loadkeymat(efile).items()) reflpos = np.loadtxt(rflfile, ndmin=2) nrefl, nrdim = reflpos.shape times = {} for elt, epos in eltspos.items(): nedim = len(epos) if not nedim <= nrdim <= nedim + 2: raise ValueError( 'Incompatible reflector and element dimensionalities') # Determine one-way distances between element and reflector centers dx = norm(epos[np.newaxis, :] - reflpos[:, :nedim], axis=-1) # Use encoded wave speed if possible, otherwise use global speed try: lc = reflpos[:, nedim] except IndexError: lc = c # Use encoded radius if possible, otherwise use global radius try: lr = reflpos[:, nedim + 1] except IndexError: lr = r # Convert distances to round-trip arrival times times[elt, elt] = 2 * (dx - lr) / lc + offset # Save the estimated arrival times savez_keymat(timefile, times)
def _throw(msg, e, sec=None): if not sec: sec = asec raise HabisConfigError.fromException(f'{msg} in [{sec}]', e)
def atimesEngine(config): ''' Use habis.trilateration.ArrivalTimeFinder to determine a set of round-trip arrival times from a set of one-to-many multipath arrival times. Multipath arrival times are computed as the maximum of cross-correlation with a reference pulse, plus some constant offset. ''' asec = 'atimes' msec = 'measurement' ssec = 'sampling' kwargs = {} def _throw(msg, e, sec=None): if not sec: sec = asec raise HabisConfigError.fromException(f'{msg} in [{sec}]', e) try: # Read all target input lists targets = sorted(k for k in config.options(asec) if k.startswith('target')) targetfiles = OrderedDict() for target in targets: targetfiles[target] = matchfiles(config.getlist(asec, target)) if len(targetfiles[target]) < 1: raise HabisConfigError(f'Key {target} matches no inputs') except Exception as e: _throw('Configuration must specify at least one unique "target" key', e) try: efiles = config.getlist(asec, 'elements', default=None) if efiles: efiles = matchfiles(efiles) kwargs['elements'] = loadmatlist(efiles, nkeys=1) except Exception as e: _throw('Invalid optional elements', e) # Grab the reference file try: reffile = config.get(msec, 'reference', default=None) except Exception as e: _throw('Invalid optional reference', e, msec) # Grab the output file try: outfile = config.get(asec, 'outfile') except Exception as e: _throw('Configuration must specify outfile', e) try: # Grab the number of processes to use (optional) nproc = config.get('general', 'nproc', mapper=int, failfunc=process.preferred_process_count) except Exception as e: _throw('Invalid optional nproc', e, 'general') try: # Determine the sampling period and a global temporal offset dt = config.get(ssec, 'period', mapper=float) t0 = config.get(ssec, 'offset', mapper=float) except Exception as e: _throw('Configuration must specify period and offset', e, ssec) # Override the number of samples in WaveformMap try: kwargs['nsamp'] = config.get(ssec, 'nsamp', mapper=int) except HabisNoOptionError: pass except Exception as e: _throw('Invalid optional nsamp', e, ssec) # Determine the oversampling rate to use when cross-correlating try: osamp = config.get(ssec, 'osamp', mapper=int, default=1) except Exception as e: _throw('Invalid optional osamp', e, ssec) try: neighbors = config.get(asec, 'neighbors', default=None) if neighbors: kwargs['neighbors'] = loadkeymat(neighbors, dtype=int) except Exception as e: _throw('Invalid optional neighbors', e) # Determine the range of elements to use; default to all (as None) try: kwargs['minsnr'] = config.getlist(asec, 'minsnr', mapper=int) except HabisNoOptionError: pass except Exception as e: _throw('Invalid optional minsnr', e) # Determine a temporal window to apply before finding delays try: kwargs['window'] = config.get(asec, 'window') except HabisNoOptionError: pass except Exception as e: _throw('Invalid optional window', e) # Determine an energy leakage threshold try: kwargs['eleak'] = config.get(asec, 'eleak', mapper=float) except HabisNoOptionError: pass except Exception as e: _throw('Invalid optional eleak', e) # Determine a temporal window to apply before finding delays try: kwargs['bandpass'] = config.get(asec, 'bandpass') except HabisNoOptionError: pass except Exception as e: _throw('Invalid optional bandpass', e) # Determine peak-selection criteria try: kwargs['peaks'] = config.get(asec, 'peaks') except HabisNoOptionError: pass except Exception as e: _throw('Invalid optional peaks', e) # Determine IMER criteria try: kwargs['imer'] = config.get(asec, 'imer') except HabisNoOptionError: pass except Exception as e: _throw('Invalid optional imer', e) maskoutliers = config.get(asec, 'maskoutliers', mapper=bool, default=False) optimize = config.get(asec, 'optimize', mapper=bool, default=False) kwargs['negcorr'] = config.get(asec, 'negcorr', mapper=bool, default=False) kwargs['signsquare'] = config.get(asec, 'signsquare', mapper=bool, default=False) kwargs['flipref'] = config.get(asec, 'flipref', mapper=bool, default=False) # Check for delay cache specifications as boolean or file suffix cachedelay = config.get(asec, 'cachedelay', default=True) if isinstance(cachedelay, bool) and cachedelay: cachedelay = 'delays.npz' try: # Remove the nearmap file key guesses = shsplit(kwargs['peaks'].pop('nearmap')) guesses = loadmatlist(guesses, nkeys=2, scalar=False) except IOError as e: guesses = None print(f'WARNING - Ignoring nearmap: {e}', file=sys.stderr) except (KeyError, TypeError, AttributeError): guesses = None else: # Adjust delay time scales guesses = {k: (v - t0) / dt for k, v in guesses.items()} # Adjust the delay time scales for the neardefault, if provided try: v = kwargs['peaks']['neardefault'] except KeyError: pass else: kwargs['peaks']['neardefault'] = (v - t0) / dt try: # Load the window map, if provided winmap = shsplit(kwargs['window'].pop('map')) winmap = loadmatlist(winmap, nkeys=2, scalar=False) except IOError as e: winmap = None print(f'WARNING - Ignoring window map: {e}', file=sys.stderr) except (KeyError, TypeError, AttributeError): winmap = None else: # Replace the map argument with the loaded array kwargs['window']['map'] = winmap times = OrderedDict() # Process each target in turn for i, (target, datafiles) in enumerate(targetfiles.items()): if guesses: # Pull the column of the nearmap for this target nearmap = {k: v[i] for k, v in guesses.items()} kwargs['peaks']['nearmap'] = nearmap if cachedelay: delayfiles = buildpaths(datafiles, extension=cachedelay) else: delayfiles = [None] * len(datafiles) times[target] = dict() dltype = 'IMER' if kwargs.get('imer', None) else 'cross-correlation' ftext = 'files' if len(datafiles) != 1 else 'file' print( f'Finding {dltype} delays for {target} ({len(datafiles)} {ftext})') for (dfile, dlayfile) in zip(datafiles, delayfiles): kwargs['cachefile'] = dlayfile delays = finddelays(nproc, dfile, reffile, osamp, **kwargs) # Note the receive channels in this data file lrx = set(k[1] for k in delays.keys()) # Convert delays to arrival times delays = {k: v * dt + t0 for k, v in delays.items()} if any(dv < 0 for dv in delays.values()): raise ValueError('Non-physical, negative delays exist') if maskoutliers: # Remove outlying values from the delay dictionary delays = stats.mask_outliers(delays) if optimize: # Prepare the arrival-time finder atf = trilateration.ArrivalTimeFinder(delays) # Compute the optimized times for this data file optimes = {(k, k): v for k, v in atf.lsmr() if k in lrx} else: # Just pass through the desired times optimes = delays times[target].update(optimes) # Build the combined times list for tmap in times.values(): try: rxset.intersection_update(tmap.keys()) except NameError: rxset = set(tmap.keys()) if not len(rxset): raise ValueError( 'Different targets have no common receive-channel indices') # Cast to Python float to avoid numpy dependencies in pickled output ctimes = {i: [float(t[i]) for t in times.values()] for i in sorted(rxset)} # Save the output as a pickled map savez_keymat(outfile, ctimes)
def _throw(msg, e): errmsg = msg + ' in [' + tsec + ']' raise HabisConfigError.fromException(errmsg, e)
def _throw(msg, e, sec=None): if not sec: sec = tsec raise HabisConfigError.fromException(msg + ' in [%s]' % (sec, ), e)
def _throw(msg, e, sec=tsec): raise HabisConfigError.fromException(msg + ' in [%s]' % (sec, ), e)