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)
else: # Store only the local portion for accumulation rows = len(times) alltimes, displs = None, None # Build an MPI datatype for the record accumulation offsets = [timetype.fields[n][1] for n in timetype.names] mtypes = [MPI.LONG] * 2 + [MPI.DOUBLE] * 1 mptype = MPI.Datatype.Create_struct([1] * len(mtypes), offsets, mtypes) mptype.Commit() # Accumulate all records on the root node comm.Gatherv([times, ntimes, mptype], [alltimes, counts, displs, mptype]) # No need for MPI datatype anymore mptype.Free() if not rank: if args.trlist: # Convert the record array to a keymap and save times = {(t, r): tm for (t, r, tm) in alltimes if not np.isnan(tm)} savez_keymat(args.output, times) else: # Find indices to sort on transmit-receive pair indx = np.lexsort((alltimes['r'], alltimes['t'])) # Rearrange the array according to the sort order np.take(alltimes, indx, out=alltimes) # Write the values, ignoring indices b = alltimes.view('float64').reshape(alltimes.shape + (-1, ))[:, 2:] mio.writebmat(b.astype('float32'), args.output)
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 finddelays(nproc=1, *args, **kwargs): ''' Distribute, among nproc processes, delay analysis for waveforms using calcdelays(). All *args and **kwargs, are passed to calcdelays on each participating process. This function explicitly sets the "queue", "rank", "grpsize", and "delaycache" arguments of calcdelays, so *args and **kwargs should not contain these values. The delaycache argument is built from an optional file specified in cachefile, which should be a map from transmit-receive pair (t, r) to a precomputed delay, loadable with habis.formats.loadkeymat. ''' forbidden = {'queue', 'rank', 'grpsize', 'delaycache'} forbidden.intersection_update(kwargs) if forbidden: raise TypeError("Forbidden argument '{next(iter(forbidden))}'") cachefile = kwargs.pop('cachefile', None) # Try to read an existing delay map try: kwargs['delaycache'] = loadkeymat(cachefile) except (KeyError, ValueError, IOError): pass # Create a result queue and a dictionary to accumulate results queue = multiprocessing.Queue(nproc) delays = {} # Extend the kwargs to include the result queue kwargs['queue'] = queue # Extend the kwargs to include the group size kwargs['grpsize'] = nproc # Keep track of waveform statistics stats = defaultdict(int) # Spawn the desired processes to perform the cross-correlation with process.ProcessPool() as pool: for i in range(nproc): # Pick a useful process name procname = process.procname(i) # Add the group rank to the kwargs kwargs['rank'] = i # Extend kwargs to contain the queue (copies kwargs) pool.addtask(target=calcdelays, name=procname, args=args, kwargs=kwargs) pool.start() # Wait for all processes to respond responses, deadpool = 0, False while responses < nproc: try: results = queue.get(timeout=0.1) except pyqueue.Empty: # Loosely join to watch for a dead pool pool.wait(timeout=0.1, limit=1) if not pool.unjoined: # Note a dead pool, give read one more try if deadpool: break else: deadpool = True else: delays.update(results[0]) for k, v in results[1].items(): if v: stats[k] += v responses += 1 if responses != nproc: print(f'WARNING: Proceeding with {responses} of {nproc} ' 'subprocess results. A subprocess may have died.') pool.wait() if stats: print(f'For file {os.path.basename(args[0])} ' f'({len(delays)} identfied times):') for k, v in sorted(stats.items()): if v: wfn = 'waveforms' if v > 1 else 'waveform' print(f' {v} {k} {wfn}') if len(delays) and cachefile: # Save the computed delays, if desired try: savez_keymat(cachefile, delays) except (ValueError, IOError): pass return delays
src, rcv = elements[t], targets[r] if not eikonal: # Use path tracing; only the path integral matters tracer.set_slowness(s) atimes[t, r] = tracer.trace(src, rcv, intonly=True) else: # Use the Eikonal solution if t != lt or tmi is None: # Compute interpolated solution for a new transmitter tmi = LinearInterpolator3D(eik.gauss(src, s)) lt = t # Interpolate the arrival time at the receiver grcv = bx.cart2cell(*rcv) atimes[t, r] = tmi.evaluate(*grcv, grad=False) if not rank and i == ipow: ipow <<= 1 print('Rank 0: Finished path %d of %d' % (i, share)) # Make sure all participants have finished MPI.COMM_WORLD.Barrier() # Collect all arrival times atimes = MPI.COMM_WORLD.gather(atimes) if not rank: # Collapse individual arrival-time maps atimes = {(t, r): v for l in atimes for (t, r), v in l.items()} savez_keymat(output, atimes)
def lsmr(self, s, epochs=1, coleq=False, tmin=0., chambolle=None, postfilter=None, partial_output=None, lsmropts={}, omega=1., bent_fallback=False, mindiff=False, save_pathmat=None, save_times=None): ''' For each of epochs rounds, compute, using LSMR, a slowness image that satisfies the straight-ray arrival-time equations implicit in this CSRTomographyTask instance. The solution is represented as a perturbation to the slowness s, an instance of habis.slowness.Slowness or its descendants, defined on the grid self.tracer.box. If coleq is True, the columns of each path-length operator will be scaled so that they all have unity norm. The value of coleq can also be a float that specifies the minimum allowable column norm (as a fraction of the maximum norm) to avoid excessive scaling of very weak columns. The LSMR implementation uses pycwp.iterative.lsmr to support arrival times distributed across multiple MPI tasks in self.comm. The keyword arguments lsmropts will be passed to lsmr to customize the solution process. Forbidden keyword arguments are "A", "b", "unorm" and "vnorm". If lsmropts contains a 'maxiter' keyword, it can be a single integer or a list of integers. If the value is a list, it provides the maximum number of LSMR iterations for each epoch in sequence. If the total number of epochs exceeds the number of values in the list, the final value will be repeated as necessary. Within each epoch, an update to the slowness image is computed based on the difference between the compensated arrival time produced by self.comptimes(s, tmin, bent_fallback) and the actual arrival times in self.atimes. When mindiff is True, compensated arrival times will be replaced by straight-ray times whenever the straight-ray time is closer to the measured data for the path. The parameter omega should be a float used to damp updates at the end of each epoch. In other words, if ds is the update computed in an epoch, the slowness s at the end of the epoch will be updated according to s <- s + omega * ds. If chambolle is not None, it should be the "weight" parameter to the function skimage.restoration.denoise_tv_chambolle. In this case, the denoising filter will be applied to the slowness image after each epoch. Alternatively, chambolle can be a list of weights, in which case it behaves like the 'maxiter' argument of lsmropts. After each epoch, if partial_output is True, a partial solution will be saved by calling self.save(partial_output, s, epoch, postfilter). The return value will be the final, perturbed solution. If postfilter is True, the solution s will be processed as postfilter(s) before it is returned. If save_pathmat is not None, it should be a string template which will be formatted with pname = save_pathmat.format(rank=self.comm.rank) and used as a file name in which the reduced path-length matrix will be stored as a COO matrix in an NPZ file with keys data, row and col, corresponding to the attributes of the COO matrix. An additional key, 'trpairs', records the transmit-receive pairs that correspond to each row in the local path matrix. If save_times is not None, it should be a string template which will be formatted as rname = save_times.format(epoch=epoch) After each epoch, an array of compensated and uncompensated straight-ray path integrals will be stored in a keymat file with name rname. All times will be coalesced onto the root rank for output. ''' if not self.isRoot: # Make sure non-root tasks are always silent lsmropts['show'] = False ncell = self.tracer.box.ncell # Set a default for Boolean coleq if coleq is True: coleq = 1e-3 elif coleq: coleq = float(coleq) # Composite slowness transform and path-length operator as CSR pathmat = (self.pathmat @ s.tosparse()).tocsr() if save_pathmat: # Save the path-length matrix pname = save_pathmat.format(rank=self.comm.rank) pcoo = pathmat.tocoo() np.savez(pname, data=pcoo.data, row=pcoo.row, col=pcoo.col, trpairs=self.trpairs) del pcoo, pname def unorm(u): # Synchronize self.comm.Barrier() # Norm of distributed vectors, to all ranks un = norm(u)**2 return np.sqrt(self.comm.allreduce(un, op=MPI.SUM)) # Process maxiter argument to allow per-epoch specification maxiter = lsmropts.pop('maxiter', None) try: maxiter = list(maxiter) except TypeError: itercounts = repeat(maxiter) else: itercounts = (maxiter[min(i, len(maxiter) - 1)] for i in count()) try: chambolle = list(chambolle) except TypeError: chamwts = repeat(chambolle) else: chamwts = (chambolle[min(i, len(chambolle) - 1)] for i in count()) msgfmt = ('Epoch {0} RMSE {1:0.6g} dsol {2:0.6g} ' 'dct {3:0.6g} dst {4:0.6g} paths {5}') epoch, sol, ltimes = 0, 0, {} ns = s.perturb(sol) while True: # Adjust RHS times with straight-ray compensation tv = self.comptimes(ns, tmin, bent_fallback) # Find RMS arrival-time error for compensated model terr = fsum((v[0] - self.atimes[k])**2 for k, v in tv.items()) tn = self.comm.allreduce(len(tv), op=MPI.SUM) terr = self.comm.allreduce(terr, op=MPI.SUM) terr = np.sqrt(terr / tn) # Compute norms of model times and time changes tdiffs = [] for k in set(tv).intersection(ltimes): a, b = tv[k] c, d = ltimes[k] tdiffs.append([(a - c)**2, (b - d)**2, a**2, b**2]) # Reduce square norms across all ranks if tdiffs: tdiffs = np.sum(tdiffs, axis=0) else: tdiffs = np.array([0.] * 4, dtype=np.float64) self.comm.Allreduce(MPI.IN_PLACE, tdiffs, op=MPI.SUM) # Set placeholder values if there is no value if tdiffs[2] == 0: tdiffs[0] = tdiffs[2] = 1. if tdiffs[3] == 0: tdiffs[1] = tdiffs[3] = 1. ltimes = tv if save_times: tv = self.comm.gather(tv) if self.isRoot: rname = save_times.format(epoch=epoch) tv = dict(kp for v in tv for kp in v.items()) savez_keymat(rname, tv) rkeys = [] rhs = [] for i, (t, r) in enumerate(self.trpairs): try: tc, ts = ltimes[t, r] ta = self.atimes[t, r] except KeyError: continue if mindiff: tc = min([tc, ts], key=lambda x: abs(x - ta)) rhs.append(ta - tc) rkeys.append(i) # Separate the RHS into row keys and values rhs = np.array(rhs) lpmat = pathmat[rkeys, :] if coleq: # Compute norms of columns of global matrix colscale = snorm(lpmat, axis=0)**2 self.comm.Allreduce(MPI.IN_PLACE, colscale, op=MPI.SUM) np.sqrt(colscale, colscale) # Clip normalization factors to avoid blow-up mxnrm = np.max(colscale) np.clip(colscale, coleq * mxnrm, mxnrm, colscale) else: colscale = None # Include column scaling in the matrix-vector product def mvp(x): if colscale is not None: x = x / colscale v = lpmat @ x return v # Transpose operation requires communications def amvp(u): # Synchronize self.comm.Barrier() # Multiple by transposed local share, then flatten v = lpmat.T @ u if colscale is not None: v /= colscale # Accumulate contributions from all ranks self.comm.Allreduce(MPI.IN_PLACE, v, op=MPI.SUM) return v # Build the linear operator representing the path matrix A = LinearOperator(shape=lpmat.shape, matvec=mvp, rmatvec=amvp, dtype=lpmat.dtype) # Use the right maxiter value for this epoch results = lsmr(A, rhs, unorm=unorm, vnorm=norm, maxiter=next(itercounts), **lsmropts) ds = results[0] if colscale is not None: ds /= colscale cmwt = next(chamwts) if cmwt: ds = denoise_tv_chambolle(s.unflatten(ds), cmwt) ds = s.flatten(ds) if self.isRoot: # Compute relative change in solution dsnrm = norm(ds) / norm(sol + ds) ctnrm = np.sqrt(tdiffs[0] / tdiffs[2]) stnrm = np.sqrt(tdiffs[1] / tdiffs[3]) print(msgfmt.format(epoch, terr, dsnrm, ctnrm, stnrm, tn)) # Update the solution sol = sol + omega * ds ns = s.perturb(sol) if partial_output: self.save(partial_output, ns, epoch, postfilter) epoch += 1 if epoch > epochs: break if postfilter: ns = postfilter(ns) return ns