/*-------------------------------------------------------------------- * $Id: fitcircle.c,v 1.3.4.2 2002/02/27 17:41:10 pwessel Exp $ * * Copyright (c) 1991-2002 by P. Wessel and W. H. F. Smith * See COPYING file for copying and redistribution conditions. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Contact info: gmt.soest.hawaii.edu *--------------------------------------------------------------------*/ /* * fitcircle [-L1] [-L2] [-S] * * Read lon,lat pairs from GMT_stdin[file]. Find mean position and pole * of best-fit circle through these points. By default, fit great * circle. If -S, fit small circle. In this case, fit great circle * first, and then search for minimum small circle by bisection. * * Formally, we want to minimize some norm on the distance between * each point and the circle, measured perpendicular to the circle. * For both L1 and L2 norms this is a rather intractable problem. * (L2 is non-linear, and in L1 it is not clear how to proceed). * However, some approximations exist which work well and are simple * to compute. We create a list of x,y,z vectors on the unit sphere, * representing the original data. To find a great circle, do this: * For L1: * Find the Fisher mean of these data, call it mean position. * Find the (Fisher) mean of all cross-products between data and * the mean position; call this the pole to the great circle. * Note that the cross-products are proportional to the distance * between datum and mean; hence above average gives data far * from mean larger weight in determining pole. This is * analogous to fitting line in plane, where data far from * average abcissa have large leverage in determining slope. * For L2: * Create 3 x 3 matrix of sums of products of data vector elements. * Find eigenvectors and eigenvalues of this matrix. * Find mean as eigenvector corresponding to max eigenvalue. * Find pole as eigenvector corresponding to min eigenvalue. * Eigenvalue-eigenvector decomposition performed by Jacobi's iterative * method of successive Givens rotations. Trials suggest that * this converges extremely rapidly (3 sweeps, 9 rotations). * * To find a small circle, first find the great circle pole and the mean * position. Suppose the small circle pole to lie in the plane containing * the mean and great circle pole, and narrow down its location by bisection. * * Author: W. H. F. Smith * Date: 16 September 1991. * Version: 3.4.1 * */ #include "gmt.h" struct DATA { double x[3]; } *data; main (int argc, char **argv) { int i, j, k, imin, imax, n_alloc, n_data, n, np, nrots, norm = -1, n_fields, n_expected_fields; BOOLEAN error = FALSE, greenwich = FALSE, find_small_circle = FALSE; double lonsum, latsum, *in; double meanv[3], cross[3], cross_sum[3], gcpole[3], scpole[3]; /* Extra vectors */ double *a, *lambda, *v, *b, *z; /* Matrix stuff */ double get_small_circle(struct DATA *data, int ndata, double *center, double *gcpole, double *scpole, int norm, double *work), rad, *work; char buffer[BUFSIZ], format[BUFSIZ]; FILE *fp = NULL; argc = GMT_begin (argc, argv); for (i = 1; i < argc; i++) { if (argv[i][0] == '-') { switch (argv[i][1]) { /* Common parameters */ case 'H': case 'V': case ':': case '\0': error += GMT_get_common_args (argv[i], 0, 0, 0, 0); break; /* Supplemental parameters */ case 'b': error += GMT_io_selection (&argv[i][2]); break; case 'L': norm = 3; if (argv[i][2]) norm = atoi(&argv[i][2]); break; case 'S': find_small_circle = TRUE; break; default: error = TRUE; GMT_default_error (argv[i][1]); break; } } else { if ((fp = GMT_fopen(argv[i], GMT_io.r_mode)) == NULL) { fprintf (stderr, "%s: Could not open file %s\n", GMT_program, argv[i]); error = TRUE; } } } if (argc == 1 || GMT_quick) { fprintf (stderr, "fitcircle %s - find best-fitting great circle to points on sphere\n\n", GMT_VERSION); fprintf(stderr,"usage: fitcircle [] -L[] [-H[]] [-S] [-V] [-:] [-bi[s][]]\n\n"); if (GMT_quick) exit (EXIT_FAILURE); fprintf(stderr,"\tReads from input_file or standard input\n"); fprintf(stderr,"\t-L specify norm as -L1 or -L2; or use -L or -L3 to give both.\n"); fprintf (stderr, "\n\tOPTIONS:\n"); GMT_explain_option ('H'); fprintf(stderr,"\t-S will attempt to fit a small circle rather than a great circle.\n"); GMT_explain_option ('V'); GMT_explain_option (':'); GMT_explain_option ('i'); GMT_explain_option ('n'); GMT_explain_option ('.'); exit (EXIT_FAILURE); } if (norm < 1 || norm > 3) { fprintf (stderr, "%s: GMT SYNTAX ERROR -L option: Choose between 1, 2, or 3\n", GMT_program); error++; } if (GMT_io.binary[0] && gmtdefs.io_header) { fprintf (stderr, "%s: GMT SYNTAX ERROR. Binary input data cannot have header -H\n", GMT_program); error++; } if (GMT_io.binary[0] && GMT_io.ncol[0] == 0) GMT_io.ncol[0] = 2; if (GMT_io.binary[0] && GMT_io.ncol[0] < 2) { fprintf (stderr, "%s: GMT SYNTAX ERROR. Binary input data (-bi) must haveat least 2 columns\n", GMT_program); error++; } if (error) exit (EXIT_FAILURE); GMT_put_history (argc, argv); /* Update .gmtcommands */ if (GMT_io.binary[0] && gmtdefs.verbose) { char *type[2] = {"double", "single"}; fprintf (stderr, "%s: Expects %d-column %s-precision binary data\n", GMT_program, GMT_io.ncol[0], type[GMT_io.single_precision[0]]); } if (fp == NULL) { fp = GMT_stdin; #ifdef SET_IO_MODE GMT_setmode (0); #endif } n_alloc = GMT_CHUNK; n_data = 0; lonsum = latsum = 0.0; sprintf (format, "%s\t%s", gmtdefs.d_format, gmtdefs.d_format); data = (struct DATA *) GMT_memory (VNULL, (size_t)n_alloc, sizeof(struct DATA), GMT_program); if (gmtdefs.io_header) for (i = 0; i < gmtdefs.n_header_recs; i++) GMT_fgets (buffer, BUFSIZ, fp); n_expected_fields = (GMT_io.ncol[0]) ? GMT_io.ncol[0] : 2; while ((n_fields = GMT_input (fp, &n_expected_fields, &in)) >= 0 && !(GMT_io.status & GMT_IO_EOF)) { /* Not yet EOF */ if (GMT_io.status & GMT_IO_MISMATCH) { fprintf (stderr, "%s: Mismatch between actual (%d) and expected (%d) fields near line %d\n", GMT_program, n_fields, n_expected_fields, n_data); exit (EXIT_FAILURE); } lonsum += in[0]; latsum += in[1]; GMT_geo_to_cart (&in[1], &in[0], data[n_data].x, TRUE); n_data++; if (n_data == n_alloc) { n_alloc += GMT_CHUNK; data = (struct DATA *) GMT_memory ((void *)data, (size_t)n_alloc, sizeof(struct DATA), GMT_program); } } if (fp != GMT_stdin) GMT_fclose(fp); data = (struct DATA *) GMT_memory ((void *)data, (size_t)n_data, sizeof(struct DATA), GMT_program); if (find_small_circle && (norm%2) ) work = (double *) GMT_memory (VNULL, (size_t)n_data, sizeof(double), GMT_program); lonsum /= n_data; latsum /= n_data; if (gmtdefs.verbose) { fprintf (stderr, "%s: %d points read, Average Position (Flat Earth): ", GMT_program, n_data); fprintf (stderr, format, lonsum, latsum); fprintf (stderr, "\n"); } if (lonsum > 180.0) greenwich = TRUE; /* Get Fisher mean in any case, in order to set L2 mean correctly, if needed. */ meanv[0] = meanv[1] = meanv[2] = 0.0; for (i = 0; i < n_data; i++) for (j = 0; j < 3; j++) meanv[j] += data[i].x[j]; GMT_normalize3v (meanv); if (norm%2) { GMT_cart_to_geo (&latsum, &lonsum, meanv, TRUE); if (greenwich && lonsum < 0.0) lonsum += 360.0; fprintf (GMT_stdout, format, lonsum, latsum); fprintf (GMT_stdout, "\tL1 Average Position (Fisher's Method)\n"); cross_sum[0] = cross_sum[1] = cross_sum[2] = 0.0; for (i = 0; i < n_data; i++) { GMT_cross3v (&data[i].x[0], meanv, cross); if (cross[2] < 0.0) { cross_sum[0] -= cross[0]; cross_sum[1] -= cross[1]; cross_sum[2] -= cross[2]; } else { cross_sum[0] += cross[0]; cross_sum[1] += cross[1]; cross_sum[2] += cross[2]; } } GMT_normalize3v (cross_sum); if (find_small_circle) for (i = 0; i < 3; i++) gcpole[i] = cross_sum[i]; GMT_cart_to_geo (&latsum, &lonsum, cross_sum, TRUE); if (greenwich && lonsum < 0.0) lonsum += 360.0; fprintf (GMT_stdout, format, lonsum, latsum); fprintf (GMT_stdout, "\tL1 N Hemisphere Great Circle Pole (Cross-Averaged)\n"); latsum = -latsum; lonsum = d_atan2(-cross_sum[1], -cross_sum[0]) * R2D; if (greenwich && lonsum < 0.0) lonsum += 360.0; fprintf (GMT_stdout, format, lonsum, latsum); fprintf (GMT_stdout, "\tL1 S Hemisphere Great Circle Pole (Cross-Averaged)\n"); if (find_small_circle) { if (gmtdefs.verbose) fprintf (stderr,"Fitting small circle using L1 norm.\n"); rad = get_small_circle (data, n_data, meanv, gcpole, scpole, 1, work); if (rad >= 0.0) { GMT_cart_to_geo (&latsum, &lonsum, scpole, TRUE); if (greenwich && lonsum < 0.0) lonsum += 360.0; fprintf (GMT_stdout, format, lonsum, latsum); fprintf (GMT_stdout, "\tL1 Small Circle Pole. "); sprintf(format, "Distance from Pole to L1 Small Circle (degrees): %s\n", gmtdefs.d_format); fprintf (GMT_stdout, format, rad); } } } sprintf (format, "%s\t%s", gmtdefs.d_format, gmtdefs.d_format); if (norm/2) { n = 3; np = n; a = (double *) GMT_memory (VNULL, (size_t)np*np, sizeof(double), GMT_program); lambda = (double *) GMT_memory (VNULL, (size_t)np, sizeof(double), GMT_program); b = (double *) GMT_memory (VNULL, (size_t)np, sizeof(double), GMT_program); z = (double *) GMT_memory (VNULL, (size_t)np, sizeof(double), GMT_program); v = (double *) GMT_memory (VNULL, (size_t)np*np, sizeof(double), GMT_program); for (i = 0; i < n_data; i++) for (j = 0; j < n; j++) for (k = 0; k < n; k++) a[j + k*np] += (data[i].x[j]*data[i].x[k]); if (GMT_jacobi (a, &n, &np, lambda, v, b, z, &nrots)) { fprintf(stderr,"%s: Eigenvalue routine failed to converge in 50 sweeps.\n", GMT_program); fprintf(stderr,"%s: The reported L2 positions might be garbage.\n", GMT_program); } if (gmtdefs.verbose) fprintf(stderr,"%s: Eigenvalue routine converged in %d rotations.\n", GMT_program, nrots); imax = 0; imin = 2; if (d_acos (GMT_dot3v (v, meanv)) > M_PI_2) for (i = 0; i < 3; i++) meanv[i] = -v[imax*np+i]; else for (i = 0; i < 3; i++) meanv[i] = v[imax*np+i]; GMT_cart_to_geo (&latsum, &lonsum, meanv, TRUE); if (greenwich && lonsum < 0.0) lonsum += 360.0; fprintf (GMT_stdout, format, lonsum, latsum); fprintf (GMT_stdout, "\tL2 Average Position (Eigenval Method)\n"); if (v[imin*np+2] < 0.0) /* Eigvec is in S Hemisphere */ for (i = 0; i < 3; i++) gcpole[i] = -v[imin*np+i]; else for (i = 0; i < 3; i++) gcpole[i] = v[imin*np+i]; GMT_cart_to_geo (&latsum, &lonsum, gcpole, TRUE); if (greenwich && lonsum < 0.0) lonsum += 360.0; fprintf (GMT_stdout, format, lonsum, latsum); fprintf (GMT_stdout, "\tL2 N Hemisphere Great Circle Pole (Eigenval Method)\n"); latsum = -latsum; lonsum = d_atan2(-gcpole[1], -gcpole[0]) * R2D; if (greenwich && lonsum < 0.0) lonsum += 360.0; fprintf (GMT_stdout, format, lonsum, latsum); fprintf (GMT_stdout, "\tL2 S Hemisphere Great Circle Pole (Eigenval Method)\n"); GMT_free ((void *)v); GMT_free ((void *)z); GMT_free ((void *)b); GMT_free ((void *)lambda); GMT_free ((void *)a); if (find_small_circle) { if (gmtdefs.verbose) fprintf(stderr,"%s: Fitting small circle using L2 norm.\n", GMT_program); rad = get_small_circle(data, n_data, meanv, gcpole, scpole, 2, work); if (rad >= 0.0) { /* True when small circle fits better than great circle */ GMT_cart_to_geo (&latsum, &lonsum, scpole, TRUE); if (greenwich && lonsum < 0.0) lonsum += 360.0; fprintf (GMT_stdout, format, lonsum, latsum); fprintf (GMT_stdout, "\tL2 Small Circle Pole. "); sprintf(format, "Distance from Pole to L2 Small Circle (degrees): %s\n", gmtdefs.d_format); fprintf (GMT_stdout, format, rad); } } } if (find_small_circle && (norm%2) ) GMT_free ((void *)work); GMT_free ((void *)data); GMT_end (argc, argv); } double get_small_circle (struct DATA *data, int ndata, double *center, double *gcpole, double *scpole, int norm, double *work) { /* Find scpole, the pole to the best-fit small circle, by L(norm) iterative search along arc between center and +/- gcpole, the pole to the best fit great circle. */ int i, j; double temppole[3], a[3], b[3], oldpole[3]; double trypos, tryneg, afit, bfit, afactor, bfactor, fit, oldfit; double length_ab, length_aold, length_bold, circle_misfit(struct DATA *data, int ndata, double *pole, int norm, double *work, double *circle_distance), circle_distance; /* First find out if solution is between center and gcpole, or center and -gcpole: */ for (i = 0; i < 3; i++) temppole[i] = (center[i] + gcpole[i]); GMT_normalize3v (temppole); trypos = circle_misfit(data, ndata, temppole, norm, work, &circle_distance); for (i = 0; i < 3; i++) temppole[i] = (center[i] - gcpole[i]); GMT_normalize3v (temppole); tryneg = circle_misfit(data, ndata, temppole, norm, work, &circle_distance); if (tryneg < trypos) { for (i = 0; i < 3; i++) a[i] = center[i]; for (i = 0; i < 3; i++) b[i] = -gcpole[i]; } else { for (i = 0; i < 3; i++) a[i] = center[i]; for (i = 0; i < 3; i++) b[i] = gcpole[i]; } /* Now a is at center and b is at pole on correct side. Try to bracket a minimum. Move from b toward a in 1 degree steps: */ afit = circle_misfit(data, ndata, a, norm, work, &circle_distance); bfit = circle_misfit(data, ndata, b, norm, work, &circle_distance); j = 1; do { afactor = sin(j * D2R); bfactor = cos(j * D2R); for (i = 0; i < 3; i++) temppole[i] = (afactor * a[i] + bfactor * b[i]); GMT_normalize3v (temppole); fit = circle_misfit(data, ndata, temppole, norm, work, &circle_distance); j++; } while (j < 90 && fit > bfit && fit > afit); if (j == 90) { /* Bad news. There isn't a better fitting pole anywhere. */ fprintf(stderr,"%s: Sorry. Cannot find small circle fitting better than great circle.\n", GMT_program); for (i = 0; i < 3; i++) scpole[i] = gcpole[i]; return(-1.0); } /* Get here when temppole points to a minimum bracketed by a and b. */ for (i = 0; i < 3; i++) oldpole[i] = temppole[i]; oldfit = fit; /* Now, while not converged, take golden section of wider interval. */ length_ab = d_acos (GMT_dot3v (a, b)); length_aold = d_acos (GMT_dot3v (a, oldpole)); length_bold = d_acos (GMT_dot3v (b, oldpole)); do { if (length_aold > length_bold) { /* Section a_old */ for (i = 0; i < 3; i++) temppole[i] = (0.38197*a[i] + 0.61803*oldpole[i]); GMT_normalize3v (temppole); fit = circle_misfit(data, ndata, temppole, norm, work, &circle_distance); if (fit < oldfit) { /* Improvement. b = oldpole, oldpole = temppole */ for (i = 0; i < 3; i++) { b[i] = oldpole[i]; oldpole[i] = temppole[i]; } oldfit = fit; } else { /* Not improved. a = temppole */ for (i = 0; i < 3; i++) a[i] = temppole[i]; } } else { /* Section b_old */ for (i = 0; i < 3; i++) temppole[i] = (0.38197*b[i] + 0.61803*oldpole[i]); GMT_normalize3v (temppole); fit = circle_misfit(data, ndata, temppole, norm, work, &circle_distance); if (fit < oldfit) { /* Improvement. a = oldpole, oldpole = temppole */ for (i = 0; i < 3; i++) { a[i] = oldpole[i]; oldpole[i] = temppole[i]; } oldfit = fit; } else { /* Not improved. b = temppole */ for (i = 0; i < 3; i++) b[i] = temppole[i]; } } length_ab = d_acos (GMT_dot3v (a, b)); length_aold = d_acos (GMT_dot3v (a, oldpole)); length_bold = d_acos (GMT_dot3v (b, oldpole)); } while (length_ab > 0.0001); /* 1 milliradian = 0.05 degree */ for (i = 0; i < 3; i++) scpole[i] = oldpole[i]; return (R2D * circle_distance); } double circle_misfit(struct DATA *data, int ndata, double *pole, int norm, double *work, double *circle_distance) { /* Find the L(norm) misfit between a small circle through center with pole pole. Return misfit in radians. */ double distance, delta_distance, misfit = 0.0; int i; /* At first, I thought we could use the center to define circle_dist = distance between pole and center. Then sum over data {dist[i] - circle_dist}. But it turns out that if the data are tightly curved, so that they are on a small circle within a few degrees of the pole, then the center point is not on the small circle, and we cannot use it. So, we first have to fit the circle_dist correctly: */ if (norm == 1) { for (i = 0; i < ndata; i++) work[i] = d_acos (GMT_dot3v (&data[i].x[0], pole)); qsort((void *)work, (size_t)ndata, sizeof(double), GMT_comp_double_asc); if (ndata%2) *circle_distance = work[ndata/2]; else *circle_distance = 0.5 * (work[(ndata/2)-1] + work[ndata/2]); } else { *circle_distance = 0.0; for (i = 0; i < ndata; i++) *circle_distance += d_acos (GMT_dot3v (&data[i].x[0], pole)); *circle_distance /= ndata; } /* Now do each data point: */ for (i = 0; i < ndata; i++) { distance = d_acos (GMT_dot3v (&data[i].x[0], pole)); delta_distance = fabs(*circle_distance - distance); misfit += ((norm == 1) ? delta_distance : delta_distance * delta_distance); } return (norm == 1) ? misfit : sqrt (misfit); }