diff --git a/knoten/bundle.py b/knoten/bundle.py index 15c4ed9ab89151e006a45ab567910eb0ed1de900..1e2ad3383b2a8306f8859965e41dee0548d6fb24 100644 --- a/knoten/bundle.py +++ b/knoten/bundle.py @@ -373,3 +373,182 @@ def compute_residuals(network, sensors): V = V.reshape(num_meas*2) return V + +def compute_sigma(V, W_parameters, W_observations): + """ + Computes the resulting standard deviation of the residuals for the current state of the bundle network. + + Parameters + ---------- + V : np.array + The control network dataframe with updated ground points + W_parameters : ndarray + The parameter weight matrix (i.e.: sensor parameters and point weights) + W_observations : ndarray + The observation weight matrix (i.e.: point weights) + + Returns + ------- + : float64 + Standard deviation of the residuals + + """ + num_parameters = W_parameters.shape[0] + num_observations = W_observations.shape[0] + dof = num_observations - num_parameters + VTPV = (V.dot(W_observations).dot(V)) + sigma0 = np.sqrt(VTPV/dof) + return sigma0 + +def bundle_iteration(J, V, W_parameters, W_observations): + """ + Parameters + ---------- + J : ndarray + The control network as a dataframe generated by plio. + V : np.array + The control network dataframe with updated ground points + W_parameters : ndarray + The parameter weight matrix (i.e.: sensor parameters and point weights) + W_observations : ndarray + The observation weight matrix (i.e.: measure weights) + + Returns + ------- + N : + """ + + N = J.T.dot(W_observations).dot(J) + W_parameters + C = J.T.dot(W_observations).dot(V) + dX = np.linalg.inv(N).dot(C) + return N, dX + +# For data snooping we need to calculate updated residuals +def compute_normalized_residual(J, V, N, W_parameters, W_observations): + """ + Computes the normalized residual statistic for the data snooping method. Method derived from + Forstner 1985 "The Reliability of Block Triangulation" + + Parameters + ---------- + V : np.array + The control network dataframe with updated ground points + N : + + W_parameters : ndarray + The parameter weight matrix (i.e.: sensor parameters and point weights) + W_observations : ndarray + The observation weight matrix (i.e.: point weights) + + Returns + ------- + : np.array + Normalized residual statistic for the data snooping + + """ + sigma0 = compute_sigma(V, W_parameters, W_observations) + Qxx = np.linalg.inv(N) + Qvv = np.linalg.inv(W_observations) - J.dot(Qxx).dot(J.T) + qvv = np.diagonal(Qvv) + sigma_vi = sigma0*np.sqrt(qvv) + wi = -V/sigma_vi + + return wi + +def check_network(network): + """ + Check that all control points in a network have at least 2 remaining measures. + + Parameters + ---------- + network : DataFrame + The control network as a dataframe generated by plio + + Returns + ------- + : list + List of measure indices that were masked out for being the only measure on a point. + """ + bad_measures = [] + for point_id, group in network.groupby('id'): + if len(group) < 2: + for measure_index, _ in group.iterrows(): + bad_measures.append(measure_index) + return bad_measures + +def data_snooping(network, sensors, parameters, k=3.29, verbose=True): + """ + Parameters + ---------- + network : DataFrame + The control network as a dataframe generated by plio + sensors : dict + A dictionary that maps ISIS serial numbers to CSM sensors + parameters : list + The list of CsmParameter to compute the partials W.R.T. + k : float64 + Critical value used for rejection criteria; defaults to Forstner's 3.29 + (or Baarda's 4.1??) + verbose : bool + If status prints should happen + + Returns + ------- + : list + Indices of the network DataFrame that were rejected during data snooping + """ + net = network + net['mask'] = False + + rejected_indices = [] + awi = np.array([5, 5, 5, 5]) #initialize larger than k so you get into first iteration + while (awi > k).any(): + + # weight matrices + coefficient_columns = compute_coefficient_columns(net[~net['mask']], sensors, parameters) + num_parameters = max(col_range[1] for col_range in coefficient_columns.values()) + W_parameters = compute_parameter_weights(net[~net['mask']], sensors, parameters, coefficient_columns) + num_observations = 2 * len(net[~net['mask']]) + W_observations = np.eye(num_observations) + + # bundle iteration (and set up) + V = compute_residuals(net[~net['mask']], sensors) + J = compute_jacobian(net[~net['mask']], sensors, parameters, coefficient_columns) + sigma0 = compute_sigma(V, W_parameters, W_observations) + N, dX = bundle_iteration(J, V, W_parameters, W_observations) + + # calculate test statistic + wi = compute_normalized_residual(J, V, N, W_parameters, W_observations) + awi = abs(wi) + + #find maximum + imax = np.argmax(awi) + if verbose: + print(f'max wi = {awi[imax]}') # display + + if awi[imax] <= k: + if verbose: + print('Data Snooping Outlier Rejection Complete') + break + + reject_index = floor(imax/2) + reject = net.index[~net['mask']][reject_index] + net.loc[reject, ['mask']] = True + rejected_indices.append(reject) + if verbose: + print(f'max wi index = {imax}') + print(f'max wi measure index = {reject_index}') + print(f'rejecting measure {net.loc[reject, ["id", "serialnumber"]].values}') + + not_enough_measures = check_network(net[~net['mask']]) + if (not_enough_measures): + for measure_index in not_enough_measures: + if verbose: + print(f'single measure point {net.loc[measure_index, "id"]}') + print(f'rejecting measure {net.loc[measure_index, ["id", "serialnumber"]].values}') + net.loc[measure_index, ['mask']] = True + + if verbose: + print('') + + return rejected_indices