-
Notifications
You must be signed in to change notification settings - Fork 250
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
Open
willschlitzer
wants to merge
40
commits into
main
Choose a base branch
from
wrap/fitcircle
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Wrap fitcircle #1550
Changes from all commits
Commits
Show all changes
40 commits
Select commit
Hold shift + click to select a range
7c1ae68
add fitcircle.py
willschlitzer 41a3ded
add fitcircle imports
willschlitzer 7e2dfee
add test_fitcircle
willschlitzer a239841
add if statements and df column names
willschlitzer d526eab
update formatting in fixture_data
willschlitzer f02466e
formatting
willschlitzer 0355a7d
add functions
willschlitzer 9bf8331
add test info to test_fitcircle_no_outfile
willschlitzer 0360abc
add if statement for normalize
willschlitzer 1e86759
add tests
willschlitzer e5b7a91
add fitcircle to index.rst
willschlitzer 3cde6d8
run make format
willschlitzer c199d09
remove unused imports
willschlitzer 899a93f
Merge branch 'main' into wrap/fitcircle
willschlitzer d131d2f
change top docstring
willschlitzer 3e57da6
Merge remote-tracking branch 'origin/wrap/fitcircle' into wrap/fitcircle
willschlitzer d4eebb7
fix variable names
willschlitzer 913a70b
Merge branch 'main' into wrap/fitcircle
willschlitzer f0033fa
Merge branch 'main' into wrap/fitcircle
willschlitzer d41d90e
Apply suggestions from code review
willschlitzer 0c11f2c
Update pygmt/src/fitcircle.py
willschlitzer a6f6265
Update pygmt/src/fitcircle.py
willschlitzer 8ab458a
Merge branch 'main' into wrap/fitcircle
willschlitzer bbbb84d
run make format
willschlitzer 00ffffd
Apply suggestions from code review
willschlitzer af7c4f9
add normalize and small_circle parameters
willschlitzer 8803d43
Merge branch 'main' into wrap/fitcircle
willschlitzer d166335
Apply suggestions from code review
willschlitzer 9227dbe
change "normalize" to "norm"
willschlitzer a10ed09
Merge branch 'main' into wrap/fitcircle
willschlitzer d75863d
Apply suggestions from code review
willschlitzer bfa8fa6
Merge branch 'main' into wrap/fitcircle
willschlitzer fa0aa4b
add docstring for "data"
willschlitzer e885b54
Update pygmt/src/fitcircle.py
willschlitzer c890477
Merge branch 'main' into wrap/fitcircle
willschlitzer bc1017a
Merge branch 'main' into wrap/fitcircle
willschlitzer ea1646d
Updates to fitcircle and test_fitcircle
willschlitzer 8bcc2ff
Add suggested fixes and test
willschlitzer 68a12b0
Merge branch 'main' into wrap/fitcircle
willschlitzer 7cb916c
Make suggested changed for alias system and parameters
willschlitzer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,7 @@ | |
| config, | ||
| dimfilter, | ||
| filter1d, | ||
| fitcircle, | ||
| grd2cpt, | ||
| grd2xyz, | ||
| grdclip, | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| """ | ||
| 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 Alias, AliasSystem | ||
| from pygmt.clib import Session | ||
| from pygmt.exceptions import GMTParameterError, GMTValueError | ||
| from pygmt.helpers import build_arg_list, fmt_docstring, validate_output_table_type | ||
|
|
||
|
|
||
| @fmt_docstring | ||
| def fitcircle( | ||
| data: PathLike | TableLike | None = None, | ||
| x=None, | ||
| y=None, | ||
| output_type: Literal["pandas", "numpy", "file"] = "pandas", | ||
| outfile: PathLike | None = None, | ||
| norm: Literal["absolutes", "squares", "both"] | None = None, | ||
| small_circle: bool | float = False, | ||
| verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | ||
| | bool = False, | ||
| **kwargs, | ||
| ) -> pd.DataFrame | np.ndarray | None: | ||
| r""" | ||
| 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 ``"absolutes"`` 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 ``"squares"`` 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. | ||
|
|
||
| Takes a matrix, (x, y) pairs, or a file name as input. | ||
|
|
||
| Must provide either ``data`` or ``x`` and ``y``. | ||
|
|
||
| Full GMT docs at :gmt-docs:`fitcircle.html`. | ||
|
|
||
| $aliases | ||
| - V = verbose | ||
|
|
||
| Parameters | ||
| ---------- | ||
| data | ||
| Pass in (longitude, latitude) or (latitude, longitude) values by | ||
| providing a file name to an ASCII data table, a 2-D | ||
| $table_classes. | ||
| x/y : 1-D arrays | ||
| Arrays of x and y coordinates of the data points. | ||
| $output_type | ||
| $outfile | ||
| norm | ||
| Specify the desired norm. Use ``"absolutes"`` or ``"squares"`` to | ||
| select a single solution, or ``"both"`` to see both solutions. Note | ||
| that ``output_type="pandas"`` is not supported when ``norm`` is | ||
| ``"both"``; use ``output_type="numpy"`` or ``output_type="file"`` | ||
| instead. | ||
| small_circle : bool or float | ||
| 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]. | ||
| $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``) | ||
| """ | ||
| if norm is None: | ||
| raise GMTParameterError(required="norm") | ||
|
|
||
| output_type = validate_output_table_type(output_type, outfile=outfile) | ||
| if output_type == "pandas" and norm == "both": | ||
| raise GMTValueError( | ||
| norm, | ||
| description="value for parameter 'norm'", | ||
| reason=( | ||
| "Pandas output is not supported when 'norm' is set to 'both' " | ||
| "since both solutions are stacked in the same rows. Use " | ||
| "output_type='numpy' or output_type='file' instead." | ||
| ), | ||
| ) | ||
|
|
||
| aliasdict = AliasSystem( | ||
| L=Alias(norm, name="norm", mapping={"absolutes": 1, "squares": 2, "both": 3}), | ||
| S=Alias(small_circle, name="small_circle"), | ||
| ).add_common( | ||
| V=verbose, | ||
| ) | ||
| aliasdict.merge(kwargs) | ||
|
|
||
| with Session() as lib: | ||
| with ( | ||
| lib.virtualfile_in( | ||
| check_kind="vector", data=data, x=x, y=y, mincols=2 | ||
| ) 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"], | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| """ | ||
| 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="squares") | ||
| 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="both", 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="both", 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="both", 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="both", 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="squares") | ||
| assert isinstance(circle_default, pd.DataFrame) | ||
| circle_array = fitcircle(data=data, norm="squares", output_type="numpy") | ||
| assert isinstance(circle_array, np.ndarray) | ||
| circle_df = fitcircle(data=data, norm="squares", output_type="pandas") | ||
| assert isinstance(circle_df, pd.DataFrame) | ||
|
|
||
|
|
||
| def test_fitcircle_pandas_unsupported_for_both_norms(data): | ||
| """ | ||
| Test that fitcircle raises an exception when output_type is "pandas" (the | ||
| default) and norm is "both", since the two 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="both") | ||
| with pytest.raises(GMTValueError): | ||
| fitcircle(data=data, norm="both", output_type="pandas") | ||
| result = fitcircle(data=data, norm="both", 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="squares", small_circle=True) | ||
| assert isinstance(result, pd.DataFrame) | ||
| assert result.shape == (5, 3) | ||
| assert "Small Circle Pole" in result.method.iloc[-1] | ||
|
|
||
|
|
||
| def test_fitcircle_input_xy(data): | ||
| """ | ||
| Run fitcircle by passing in x/y as input. | ||
| """ | ||
| result = fitcircle(x=data.longitude, y=data.latitude, norm="squares") | ||
| assert isinstance(result, pd.DataFrame) | ||
| assert result.shape == (4, 3) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
These output columns might change depending on whether
-L/norm=1/2 or-L/norm=3/True ? Could we do something like:or whatever column names make sense for
norm=3.Alternatively, we can just disable the ability to output to
pandasformat fornorm=3/norm=Trueif it gets too complicated.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.
Decided to disable the pandas format for norm=3/True, as that seemed more straightforward.