-
Notifications
You must be signed in to change notification settings - Fork 255
Wrap fitcircle #1550
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Wrap fitcircle #1550
Changes from 39 commits
7c1ae68
41a3ded
7e2dfee
a239841
d526eab
f02466e
0355a7d
9bf8331
0360abc
1e86759
e5b7a91
3cde6d8
c199d09
899a93f
d131d2f
3e57da6
d4eebb7
913a70b
f0033fa
d41d90e
0c11f2c
a6f6265
8ab458a
bbbb84d
00ffffd
af7c4f9
8803d43
d166335
9227dbe
a10ed09
d75863d
bfa8fa6
fa0aa4b
e885b54
c890477
bc1017a
ea1646d
8bcc2ff
68a12b0
7cb916c
6f0e501
0e196f8
eff642e
4cd6702
ff4c77c
3be5bac
5fdfede
a6777d8
6bd302a
cdf4ef9
59afc1f
ec5deec
c0d0423
696a96d
5856e64
dd714ee
961ad92
efbe10d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -129,6 +129,7 @@ Operations on tabular data | |
| blockmedian | ||
| blockmode | ||
| filter1d | ||
| fitcircle | ||
| nearneighbor | ||
| project | ||
| select | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,7 @@ | |
| config, | ||
| dimfilter, | ||
| filter1d, | ||
| fitcircle, | ||
| grd2cpt, | ||
| grd2xyz, | ||
| grdclip, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| """ | ||
| fitcircle - Find mean position and great [or small] circle fit to points on | ||
| sphere. | ||
| """ | ||
|
|
||
| from typing import Literal | ||
|
|
||
| import numpy as np | ||
| import pandas as pd | ||
| from pygmt._typing import PathLike, TableLike | ||
| from pygmt.alias import AliasSystem | ||
| from pygmt.clib import Session | ||
| from pygmt.exceptions import GMTParameterError, GMTValueError | ||
| from pygmt.helpers import ( | ||
| build_arg_list, | ||
| fmt_docstring, | ||
| use_alias, | ||
| validate_output_table_type, | ||
| ) | ||
|
|
||
|
|
||
| @fmt_docstring | ||
| @use_alias( | ||
| L="norm", | ||
| S="small_circle", | ||
| ) | ||
|
seisman marked this conversation as resolved.
Outdated
|
||
| def fitcircle( | ||
| data: PathLike | TableLike, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should support parameters |
||
| output_type: Literal["pandas", "numpy", "file"] = "pandas", | ||
| outfile: PathLike | None = None, | ||
| verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | ||
| | bool = False, | ||
| **kwargs, | ||
| ) -> pd.DataFrame | np.ndarray | None: | ||
| r""" | ||
|
willschlitzer marked this conversation as resolved.
Outdated
|
||
| Find mean position and great [or small] circle fit to points on sphere. | ||
|
|
||
| **fitcircle** reads (longitude, latitude) or (latitude, longitude) values from the | ||
| first two columns of the input data. These are converted to Cartesian | ||
| three-vectors on the unit sphere. Then two locations are found: the mean | ||
| of the input positions, and the pole to the great circle which best fits | ||
| the input positions. The user may choose one or both of two possible | ||
| solutions to this problem. When the data are closely grouped along a | ||
| great circle both solutions are similar. If the data have large | ||
| dispersion, the pole to the great circle will be less well determined | ||
| than the mean. Compare both solutions as a qualitative check. | ||
|
|
||
| Setting ``norm`` to **1** approximates the minimization of the sum of | ||
| absolute values of cosines of angular distances. This solution finds the | ||
| mean position as the Fisher average of the data, and the pole position | ||
| as the Fisher average of the cross-products between the mean and the | ||
| data. Averaging cross-products gives weight to points in proportion to | ||
| their distance from the mean, analogous to the "leverage" of distant | ||
| points in linear regression in the plane. | ||
|
|
||
| Setting ``norm`` to **2** approximates the minimization of the sum of | ||
| squares of cosines of angular distances. It creates a 3 by 3 matrix of | ||
| sums of squares of components of the data vectors. The eigenvectors of | ||
| this matrix give the mean and pole locations. This method may be more | ||
| subject to roundoff errors when there are thousands of data. The pole is | ||
| given by the eigenvector corresponding to the smallest eigenvalue; it is | ||
| the least-well represented factor in the data and is not easily | ||
| estimated by either method. | ||
|
|
||
| Full GMT docs at :gmt-docs:`fitcircle.html`. | ||
|
|
||
| $aliases | ||
| - V = verbose | ||
|
willschlitzer marked this conversation as resolved.
Outdated
|
||
|
|
||
| Parameters | ||
| ---------- | ||
| data | ||
| Pass in (longitude, latitude) or (latitude, longitude) values by | ||
|
willschlitzer marked this conversation as resolved.
Outdated
|
||
| providing a file name to an ASCII data table, a 2-D | ||
| $table_classes. | ||
| $output_type | ||
| $outfile | ||
| norm : int or bool | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1, 2, 3 are not readable arguments. GMT recommends As for the parameter, GMT supports |
||
| Specify the desired *norm* as **1** or **2**\ , or use ``True`` or | ||
| **3** to see both solutions. Note that ``output_type="pandas"`` is | ||
| not supported when ``norm`` is ``True`` or **3**; use | ||
| ``output_type="numpy"`` or ``output_type="file"`` instead. | ||
| small_circle : bool or float | ||
|
willschlitzer marked this conversation as resolved.
Outdated
|
||
| Attempt to fit a small circle instead of a great circle. The pole | ||
| will be constrained to lie on the great circle connecting the pole | ||
| of the best-fit great circle and the mean location of the data. | ||
| Optionally append the desired fixed latitude of the small circle | ||
| [Default will determine the optimal latitude]. | ||
|
willschlitzer marked this conversation as resolved.
Outdated
|
||
| $verbose | ||
|
|
||
| Returns | ||
| ------- | ||
| ret | ||
| Return type depends on ``outfile`` and ``output_type``: | ||
|
|
||
| - ``None`` if ``outfile`` is set (output will be stored in the file set by | ||
| ``outfile``) | ||
| - :class:`pandas.DataFrame` or :class:`numpy.ndarray` if ``outfile`` is not set | ||
| (depends on ``output_type``) | ||
| """ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See comment https://github.com/GenericMappingTools/pygmt/pull/1550/changes#r3829465312. Need to check if |
||
| if kwargs.get("L") is None: | ||
| raise GMTParameterError(required="norm") | ||
|
|
||
| output_type = validate_output_table_type(output_type, outfile=outfile) | ||
| norm = kwargs.get("L") | ||
| if output_type == "pandas" and (norm is True or norm == 3): | ||
| raise GMTValueError( | ||
| norm, | ||
| description="value for parameter 'norm'", | ||
| reason=( | ||
| "Pandas output is not supported when 'norm' is set to True or 3 " | ||
| "since both L1 and L2 solutions are stacked in the same rows. " | ||
| "Use output_type='numpy' or output_type='file' instead." | ||
| ), | ||
| ) | ||
|
|
||
| aliasdict = AliasSystem().add_common( | ||
| V=verbose, | ||
| ) | ||
| aliasdict.merge(kwargs) | ||
|
|
||
| with Session() as lib: | ||
| with ( | ||
| lib.virtualfile_in(check_kind="vector", data=data) as vintbl, | ||
| lib.virtualfile_out(kind="dataset", fname=outfile) as vouttbl, | ||
| ): | ||
| lib.call_module( | ||
| module="fitcircle", | ||
| args=build_arg_list(aliasdict, infile=vintbl, outfile=vouttbl), | ||
| ) | ||
| return lib.virtualfile_to_dataset( | ||
| vfname=vouttbl, | ||
| output_type=output_type, | ||
| column_names=["longitude", "latitude", "method"], | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These output columns might change depending on whether column_names=["longitude", "latitude", "method"] if kwargs.get("L") in {1, 2} else ["longitude_l1", "latitude_l1", "longitude_l2", "latitude_l2"],or whatever column names make sense for Alternatively, we can just disable the ability to output to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Decided to disable the pandas format for norm=3/True, as that seemed more straightforward. |
||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| """ | ||
| Test pygmt.fitcircle. | ||
| """ | ||
|
|
||
| from pathlib import Path | ||
|
|
||
| import numpy as np | ||
| import numpy.testing as npt | ||
| import pandas as pd | ||
| import pytest | ||
| from pygmt import fitcircle | ||
| from pygmt.exceptions import GMTParameterError, GMTValueError | ||
| from pygmt.helpers import GMTTempFile | ||
| from pygmt.src import which | ||
|
|
||
|
|
||
| @pytest.fixture(scope="module", name="data") | ||
| def fixture_data(): | ||
| """ | ||
| Load the sample data from the @sat_03 remote file. | ||
| """ | ||
| fname = which("@sat_03.txt", download="c") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This |
||
| return pd.read_csv( | ||
| fname, header=None, skiprows=1, sep="\t", names=["longitude", "latitude", "z"] | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.benchmark | ||
| def test_fitcircle_no_outfile(data): | ||
| """ | ||
| Test fitcircle with no set outfile. | ||
| """ | ||
| result = fitcircle(data=data, norm=2) | ||
| assert isinstance(result, pd.DataFrame) | ||
| assert result.shape == (4, 3) | ||
| # Test longitude results | ||
| npt.assert_allclose(result.longitude.min(), 52.7449849947) | ||
| npt.assert_allclose(result.longitude.max(), 330.243649573) | ||
| # Test latitude results | ||
| npt.assert_allclose(result.latitude.min(), -21.2046833116) | ||
| npt.assert_allclose(result.latitude.max(), 21.2046833116) | ||
|
|
||
|
|
||
| def test_fitcircle_file_output(data): | ||
| """ | ||
| Test that fitcircle returns a file output when it is specified. | ||
| """ | ||
| with GMTTempFile(suffix=".txt") as tmpfile: | ||
| result = fitcircle( | ||
| data=data, norm=True, outfile=tmpfile.name, output_type="file" | ||
| ) | ||
| assert result is None # return value is None | ||
| assert Path(tmpfile.name).stat().st_size > 0 # check that outfile exists | ||
|
|
||
|
|
||
| def test_fitcircle_invalid_format(data): | ||
| """ | ||
| Test that fitcircle fails with an incorrect format for output_type. | ||
| """ | ||
| with pytest.raises(GMTValueError): | ||
| fitcircle(data=data, norm=True, output_type="a") | ||
|
|
||
|
|
||
| def test_fitcircle_no_norm(data): | ||
| """ | ||
| Test that fitcircle fails when the required "norm" parameter is missing. | ||
| """ | ||
| with pytest.raises(GMTParameterError): | ||
| fitcircle(data=data) | ||
|
|
||
|
|
||
| def test_fitcircle_no_outfile_specified(data): | ||
| """ | ||
| Test that fitcircle fails when output_type is set to "file" but no outfile | ||
| is specified. | ||
| """ | ||
| with pytest.raises(GMTParameterError): | ||
| fitcircle(data=data, norm=True, output_type="file") | ||
|
|
||
|
|
||
| def test_fitcircle_outfile_incorrect_output_type(data): | ||
| """ | ||
| Test that fitcircle raises a warning when an outfile filename is set but the | ||
| output_type is not set to "file". | ||
| """ | ||
| with GMTTempFile(suffix=".txt") as tmpfile: | ||
| with pytest.warns(RuntimeWarning) as record: | ||
| result = fitcircle( | ||
| data=data, norm=True, outfile=tmpfile.name, output_type="numpy" | ||
| ) | ||
| assert len(record) == 1 # check that only one warning was raised | ||
| assert result is None # return value is None | ||
| assert Path(tmpfile.name).stat().st_size > 0 # check that outfile exists | ||
|
|
||
|
|
||
| def test_fitcircle_format(data): | ||
| """ | ||
| Test that correct formats are returned. | ||
| """ | ||
| circle_default = fitcircle(data=data, norm=2) | ||
| assert isinstance(circle_default, pd.DataFrame) | ||
| circle_array = fitcircle(data=data, norm=2, output_type="numpy") | ||
| assert isinstance(circle_array, np.ndarray) | ||
| circle_df = fitcircle(data=data, norm=2, output_type="pandas") | ||
| assert isinstance(circle_df, pd.DataFrame) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("norm", [True, 3]) | ||
| def test_fitcircle_pandas_unsupported_for_both_norms(data, norm): | ||
| """ | ||
| Test that fitcircle raises an exception when output_type is "pandas" (the | ||
| default) and norm is True or 3, since the L1 and L2 solutions are stacked | ||
| in the same rows and can't be represented as a single pandas.DataFrame. | ||
| """ | ||
| with pytest.raises(GMTValueError): | ||
| fitcircle(data=data, norm=norm) | ||
| with pytest.raises(GMTValueError): | ||
| fitcircle(data=data, norm=norm, output_type="pandas") | ||
| result = fitcircle(data=data, norm=norm, output_type="numpy") | ||
| assert isinstance(result, np.ndarray) | ||
|
|
||
|
|
||
| def test_fitcircle_small_circle(data): | ||
| """ | ||
| Test that fitcircle can fit a small circle instead of a great circle. | ||
| """ | ||
| result = fitcircle(data=data, norm=2, small_circle=True) | ||
| assert isinstance(result, pd.DataFrame) | ||
| assert result.shape == (5, 3) | ||
| assert "Small Circle Pole" in result.method.iloc[-1] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move this file to Line 130.