From 7c1ae68443d772cffb77a09e93587fcf3a91420c Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Sun, 26 Sep 2021 11:30:04 +0100 Subject: [PATCH 01/45] add fitcircle.py --- pygmt/src/fitcircle.py | 102 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 pygmt/src/fitcircle.py diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py new file mode 100644 index 00000000000..b017b167e8c --- /dev/null +++ b/pygmt/src/fitcircle.py @@ -0,0 +1,102 @@ +""" +filter1d - Time domain filtering of 1-D data tables +""" +import warnings + +import pandas as pd +import xarray as xr +from pygmt.clib import Session +from pygmt.exceptions import GMTInvalidInput +from pygmt.helpers import ( + GMTTempFile, + build_arg_string, + fmt_docstring, + kwargs_to_strings, + use_alias, +) + + +@fmt_docstring +@use_alias( + L="normalize", + S="small_circle", + V="verbose", +) +def fitcircle(data, output_type="pandas", outfile=None, **kwargs): + r""" + Fit coordinators to create vectors on a sphere. + + **fitcircle** reads lon,lat [or lat,lon] values from the first two + columns on standard input [or *table*]. 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. The first is called **-L1** and + the second is called **-L2**. 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. + + The **-L1** solution is so called because it 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. + + The **-L2** solution is so called because it 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 option list at :gmt-docs:`fitcircle.html` + + {aliases} + + Parameters + ----------. + output_type : str + Determine the format the xyz data will be returned in [Default is + ``pandas``]: + + - ``numpy`` - :class:`numpy.ndarray` + - ``pandas``- :class:`pandas.DataFrame` + - ``file`` - ASCII file (requires ``outfile``) + outfile : str + The file name for the output ASCII file. + + Returns + ------- + ret : pandas.DataFrame or numpy.ndarray or None + Return type depends on ``outfile`` and ``output_type``: + + - None if ``outfile`` is set (output will be stored in file set by + ``outfile``) + - :class:`pandas.DataFrame` or :class:`numpy.ndarray` if ``outfile`` is + not set (depends on ``output_type`` [Default is + :class:`pandas.DataFrame`]) + + """ + with GMTTempFile() as tmpfile: + with Session() as lib: + file_context = lib.virtualfile_from_data(check_kind="vector", data=data) + with file_context as infile: + if outfile is None: + outfile = tmpfile.name + arg_str = " ".join([infile, build_arg_string(kwargs), "->" + outfile]) + lib.call_module("fitcircle", arg_str) + + # Read temporary csv output to a pandas table + if outfile == tmpfile.name: # if user did not set outfile, return pd.DataFrame + result = pd.read_csv(tmpfile.name, sep="\t", comment=">") + elif outfile != tmpfile.name: # return None if outfile set, output in outfile + result = None + + if output_type == "numpy": + result = result.to_numpy() + return result From 41a3ded5ba9488ef59e48cbf03b95a5f987801f6 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Tue, 28 Sep 2021 08:33:12 +0100 Subject: [PATCH 02/45] add fitcircle imports --- pygmt/__init__.py | 1 + pygmt/src/__init__.py | 1 + pygmt/src/fitcircle.py | 9 ++++----- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pygmt/__init__.py b/pygmt/__init__.py index 7a1a1079f37..a08a1ad5e33 100644 --- a/pygmt/__init__.py +++ b/pygmt/__init__.py @@ -34,6 +34,7 @@ blockmedian, blockmode, config, + fitcircle, grd2cpt, grd2xyz, grdclip, diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index a3dded681eb..d9c829d2e9a 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -9,6 +9,7 @@ from pygmt.src.colorbar import colorbar from pygmt.src.config import config from pygmt.src.contour import contour +from pygmt.src.fitcircle import fitcircle from pygmt.src.grd2cpt import grd2cpt from pygmt.src.grd2xyz import grd2xyz from pygmt.src.grdclip import grdclip diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index b017b167e8c..d7bd6dda12a 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -31,13 +31,12 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): 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. The first is called **-L1** and - the second is called **-L2**. When the data are closely grouped along a - great circle both solutions are similar. If the data have large + 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. - The **-L1** solution is so called because it approximates the + Setting `normalize` 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 @@ -45,7 +44,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): 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. - The **-L2** solution is so called because it approximates the + Setting `normalize` 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 From 7e2dfeeff1c3bc8b756812ece351a97cd192a458 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Tue, 28 Sep 2021 09:00:22 +0100 Subject: [PATCH 03/45] add test_fitcircle --- pygmt/tests/test_fitcircle.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 pygmt/tests/test_fitcircle.py diff --git a/pygmt/tests/test_fitcircle.py b/pygmt/tests/test_fitcircle.py new file mode 100644 index 00000000000..9aad3dbb89e --- /dev/null +++ b/pygmt/tests/test_fitcircle.py @@ -0,0 +1,23 @@ +""" +Tests for fitcircle. +""" + +import os + +import numpy as np +import pandas as pd +import pytest +from pygmt import fitcircle +from pygmt.exceptions import GMTInvalidInput +from pygmt.helpers import GMTTempFile +from pygmt.src import which + + +@pytest.fixture(scope="module", name="data") +def fixture_table(): + """ + Load the sample data from the sat_03 remote file. + """ + fname = which("@sat_03.txt", download="c") + data = pd.read_csv(fname, header=None, skiprows=1, sep="\t",) + return data \ No newline at end of file From a239841ed05d0b733e536f7020bb92e35488d0ae Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Tue, 28 Sep 2021 09:16:22 +0100 Subject: [PATCH 04/45] add if statements and df column names --- pygmt/src/fitcircle.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index d7bd6dda12a..6408295f729 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -31,7 +31,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): 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 + 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. @@ -81,6 +81,20 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): :class:`pandas.DataFrame`]) """ + if output_type not in ["numpy", "pandas", "file"]: + raise GMTInvalidInput( + """Must specify format as either numpy, pandas, or file.""" + ) + if outfile is not None and output_type != "file": + msg = ( + f"Changing `output_type` of fitcirle from '{output_type}' to 'file' " + "since `outfile` parameter is set. Please use `output_type='file'` " + "to silence this warning." + ) + warnings.warn(msg, category=RuntimeWarning, stacklevel=2) + output_type = "file" + elif output_type == "file" and outfile is None: + raise GMTInvalidInput("""Must specify outfile for ASCII output.""") with GMTTempFile() as tmpfile: with Session() as lib: file_context = lib.virtualfile_from_data(check_kind="vector", data=data) @@ -92,7 +106,12 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): # Read temporary csv output to a pandas table if outfile == tmpfile.name: # if user did not set outfile, return pd.DataFrame - result = pd.read_csv(tmpfile.name, sep="\t", comment=">") + result = pd.read_csv( + tmpfile.name, + sep="\t", + names=["longitutde", "latitude", "method"], + comment=">", + ) elif outfile != tmpfile.name: # return None if outfile set, output in outfile result = None From d526eab6cf1a1facf28aafc690e0520dbe59b813 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Tue, 28 Sep 2021 09:21:02 +0100 Subject: [PATCH 05/45] update formatting in fixture_data --- pygmt/tests/test_fitcircle.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pygmt/tests/test_fitcircle.py b/pygmt/tests/test_fitcircle.py index 9aad3dbb89e..670fe7c70ab 100644 --- a/pygmt/tests/test_fitcircle.py +++ b/pygmt/tests/test_fitcircle.py @@ -14,10 +14,16 @@ @pytest.fixture(scope="module", name="data") -def fixture_table(): +def fixture_data(): """ Load the sample data from the sat_03 remote file. """ fname = which("@sat_03.txt", download="c") - data = pd.read_csv(fname, header=None, skiprows=1, sep="\t",) - return data \ No newline at end of file + data = pd.read_csv( + fname, + header=None, + skiprows=1, + sep="\t", + names=["longitutde", "latitude", "z"], + ) + return data From f02466e64fee294cd8d942d1ad696f22f52064e9 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Wed, 29 Sep 2021 07:37:26 +0100 Subject: [PATCH 06/45] formatting --- pygmt/src/fitcircle.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 6408295f729..a7fb79d7d1c 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -42,7 +42,8 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): 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. + analogous to the "leverage" of distant points in linear regression in + the plane. Setting `normalize` to **2** approximates the minimization of the sum of squares of cosines of angular distances. It @@ -51,7 +52,8 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): 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. + represented factor in the data and is not easily estimated by either + method. Full option list at :gmt-docs:`fitcircle.html` From 0355a7dedbb2344a79279e6990e2b2a1faaf8ba3 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Wed, 29 Sep 2021 07:37:34 +0100 Subject: [PATCH 07/45] add functions --- pygmt/tests/test_fitcircle.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pygmt/tests/test_fitcircle.py b/pygmt/tests/test_fitcircle.py index 670fe7c70ab..0ed47fcfb9b 100644 --- a/pygmt/tests/test_fitcircle.py +++ b/pygmt/tests/test_fitcircle.py @@ -27,3 +27,23 @@ def fixture_data(): names=["longitutde", "latitude", "z"], ) return data + + +def test_fitcircle_no_outfile(data): + """ + Test fitcircle with no set outfile. + """ + result = fitcircle(data=data, normalize=True) + assert result.shape == (7, 3) + + +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, normalize=True, outfile=tmpfile.name, output_type="file" + ) + assert result is None # return value is None + assert os.path.exists(path=tmpfile.name) # check that outfile exists From 9bf83311948bb7c5a10be9b562ba69a439e960e8 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Wed, 29 Sep 2021 07:50:18 +0100 Subject: [PATCH 08/45] add test info to test_fitcircle_no_outfile --- pygmt/tests/test_fitcircle.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pygmt/tests/test_fitcircle.py b/pygmt/tests/test_fitcircle.py index 0ed47fcfb9b..589b7281c11 100644 --- a/pygmt/tests/test_fitcircle.py +++ b/pygmt/tests/test_fitcircle.py @@ -6,6 +6,7 @@ import numpy as np import pandas as pd +import numpy.testing as npt import pytest from pygmt import fitcircle from pygmt.exceptions import GMTInvalidInput @@ -35,7 +36,16 @@ def test_fitcircle_no_outfile(data): """ result = fitcircle(data=data, normalize=True) assert result.shape == (7, 3) - + # Test longitude results + npt.assert_allclose(result.iloc[:,0].min(), 52.7434273422) + npt.assert_allclose(result.iloc[:,0].max(), 330.243649573) + npt.assert_allclose(result.iloc[:,0].mean(), 223.078116476) + npt.assert_allclose(result.iloc[:,0].median(), 232.7449849) + # Test latitude results + npt.assert_allclose(result.iloc[:,1].min(), -21.2085369093) + npt.assert_allclose(result.iloc[:,1].max(), 21.2085369093) + npt.assert_allclose(result.iloc[:,1].mean(), -7.8863683297) + npt.assert_allclose(result.iloc[:,1].median(), -18.406777) def test_fitcircle_file_output(data): """ From 0360abc5642df6bcb2470b486cc3da41c315b52e Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Wed, 29 Sep 2021 08:05:48 +0100 Subject: [PATCH 09/45] add if statement for normalize --- pygmt/src/fitcircle.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index a7fb79d7d1c..4408857c45a 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -83,6 +83,8 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): :class:`pandas.DataFrame`]) """ + if "L" not in kwargs: + raise GMTInvalidInput("""Pass a required argument to 'normalize'.""") if output_type not in ["numpy", "pandas", "file"]: raise GMTInvalidInput( """Must specify format as either numpy, pandas, or file.""" From 1e86759f7b85264b0b37145770607d8aa7bd80d7 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Wed, 29 Sep 2021 08:05:54 +0100 Subject: [PATCH 10/45] add tests --- pygmt/tests/test_fitcircle.py | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/pygmt/tests/test_fitcircle.py b/pygmt/tests/test_fitcircle.py index 589b7281c11..ce2fab12651 100644 --- a/pygmt/tests/test_fitcircle.py +++ b/pygmt/tests/test_fitcircle.py @@ -57,3 +57,53 @@ def test_fitcircle_file_output(data): ) assert result is None # return value is None assert os.path.exists(path=tmpfile.name) # check that outfile exists + +def test_fitcircle_invalid_format(data): + """ + Test that fitcircle fails with an incorrect format for output_type. + """ + with pytest.raises(GMTInvalidInput): + fitcircle(data=data, normalize=True, output_type="a") + + +def test_fitcircle_no_normalize(data): + """ + Test that fitcircle fails with an argument is missing for normalize. + """ + with pytest.raises(GMTInvalidInput): + fitcircle(data=data) + + +def test_fitcircle_no_outfile_specified(data): + """ + Test that fitcircle fails when outpput_type is set to 'file' but + no output file name is specified. + """ + with pytest.raises(GMTInvalidInput): + fitcircle(data=data, normalize=True, output_type="file") + + +def test_filter1d_outfile_incorrect_output_type(data): + """ + Test that filter1d raises a warning when an outfile filename is set but the + output_type is not set to 'file'. + """ + with pytest.warns(RuntimeWarning): + with GMTTempFile(suffix=".txt") as tmpfile: + result = fitcircle( + data=data, normalize=True, outfile=tmpfile.name, output_type="numpy" + ) + assert result is None # return value is None + assert os.path.exists(path=tmpfile.name) # check that outfile exists + + +def test_fitcircle_format(data): + """ + Test that correct formats are returned. + """ + circle_default = fitcircle(data=data, normalize=True) + assert isinstance(circle_default, pd.DataFrame) + circle_default = fitcircle(data=data, normalize=True, output_type="numpy") + assert isinstance(circle_default, np.ndarray) + circle_default = fitcircle(data=data, normalize=True, output_type="pandas") + assert isinstance(circle_default, pd.DataFrame) From e5b7a91f2ed9a67c4b3907042bc33626ff4ddb65 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Sat, 6 Nov 2021 08:06:04 +0000 Subject: [PATCH 11/45] add fitcircle to index.rst --- doc/api/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/api/index.rst b/doc/api/index.rst index 69685060086..64f7b4e30ea 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -82,6 +82,7 @@ Operations on tabular data: blockmean blockmedian blockmode + fitcircle nearneighbor surface From 3cde6d819b442d7d1859805bafbe9e5b71ab09e2 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Sat, 6 Nov 2021 08:06:53 +0000 Subject: [PATCH 12/45] run make format --- pygmt/tests/test_fitcircle.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/pygmt/tests/test_fitcircle.py b/pygmt/tests/test_fitcircle.py index ce2fab12651..3f9abe38e98 100644 --- a/pygmt/tests/test_fitcircle.py +++ b/pygmt/tests/test_fitcircle.py @@ -5,8 +5,8 @@ import os import numpy as np -import pandas as pd import numpy.testing as npt +import pandas as pd import pytest from pygmt import fitcircle from pygmt.exceptions import GMTInvalidInput @@ -37,15 +37,16 @@ def test_fitcircle_no_outfile(data): result = fitcircle(data=data, normalize=True) assert result.shape == (7, 3) # Test longitude results - npt.assert_allclose(result.iloc[:,0].min(), 52.7434273422) - npt.assert_allclose(result.iloc[:,0].max(), 330.243649573) - npt.assert_allclose(result.iloc[:,0].mean(), 223.078116476) - npt.assert_allclose(result.iloc[:,0].median(), 232.7449849) + npt.assert_allclose(result.iloc[:, 0].min(), 52.7434273422) + npt.assert_allclose(result.iloc[:, 0].max(), 330.243649573) + npt.assert_allclose(result.iloc[:, 0].mean(), 223.078116476) + npt.assert_allclose(result.iloc[:, 0].median(), 232.7449849) # Test latitude results - npt.assert_allclose(result.iloc[:,1].min(), -21.2085369093) - npt.assert_allclose(result.iloc[:,1].max(), 21.2085369093) - npt.assert_allclose(result.iloc[:,1].mean(), -7.8863683297) - npt.assert_allclose(result.iloc[:,1].median(), -18.406777) + npt.assert_allclose(result.iloc[:, 1].min(), -21.2085369093) + npt.assert_allclose(result.iloc[:, 1].max(), 21.2085369093) + npt.assert_allclose(result.iloc[:, 1].mean(), -7.8863683297) + npt.assert_allclose(result.iloc[:, 1].median(), -18.406777) + def test_fitcircle_file_output(data): """ @@ -58,6 +59,7 @@ def test_fitcircle_file_output(data): assert result is None # return value is None assert os.path.exists(path=tmpfile.name) # check that outfile exists + def test_fitcircle_invalid_format(data): """ Test that fitcircle fails with an incorrect format for output_type. @@ -76,8 +78,8 @@ def test_fitcircle_no_normalize(data): def test_fitcircle_no_outfile_specified(data): """ - Test that fitcircle fails when outpput_type is set to 'file' but - no output file name is specified. + Test that fitcircle fails when outpput_type is set to 'file' but no output + file name is specified. """ with pytest.raises(GMTInvalidInput): fitcircle(data=data, normalize=True, output_type="file") From c199d09f1b0f81eb7be39c36b7dc6d7daa6e313e Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Sat, 6 Nov 2021 08:13:10 +0000 Subject: [PATCH 13/45] remove unused imports --- pygmt/src/fitcircle.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 4408857c45a..034f9b5575b 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -4,16 +4,9 @@ import warnings import pandas as pd -import xarray as xr from pygmt.clib import Session from pygmt.exceptions import GMTInvalidInput -from pygmt.helpers import ( - GMTTempFile, - build_arg_string, - fmt_docstring, - kwargs_to_strings, - use_alias, -) +from pygmt.helpers import GMTTempFile, build_arg_string, fmt_docstring, use_alias @fmt_docstring From d131d2fc3b13b1c562f61e2f8f516ca5c5a1fc8e Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Sat, 6 Nov 2021 08:19:49 +0000 Subject: [PATCH 14/45] change top docstring --- pygmt/src/fitcircle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 034f9b5575b..ef86069c9df 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -1,5 +1,5 @@ """ -filter1d - Time domain filtering of 1-D data tables +fitcircle - Fit coordinators to create vectors on a sphere. """ import warnings From d4eebb79e01fdb625ca5093c58f2aac9f33369bd Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Fri, 14 Jan 2022 00:39:15 +0000 Subject: [PATCH 15/45] fix variable names --- pygmt/tests/test_fitcircle.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pygmt/tests/test_fitcircle.py b/pygmt/tests/test_fitcircle.py index 3f9abe38e98..0da9469cf8d 100644 --- a/pygmt/tests/test_fitcircle.py +++ b/pygmt/tests/test_fitcircle.py @@ -105,7 +105,7 @@ def test_fitcircle_format(data): """ circle_default = fitcircle(data=data, normalize=True) assert isinstance(circle_default, pd.DataFrame) - circle_default = fitcircle(data=data, normalize=True, output_type="numpy") - assert isinstance(circle_default, np.ndarray) - circle_default = fitcircle(data=data, normalize=True, output_type="pandas") - assert isinstance(circle_default, pd.DataFrame) + circle_array = fitcircle(data=data, normalize=True, output_type="numpy") + assert isinstance(circle_array, np.ndarray) + circle_df = fitcircle(data=data, normalize=True, output_type="pandas") + assert isinstance(circle_df, pd.DataFrame) From d41d90ef8acd6e0e71a00f326e754bf3ee268857 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Tue, 19 Apr 2022 09:28:18 +0100 Subject: [PATCH 16/45] Apply suggestions from code review Co-authored-by: Dongdong Tian --- pygmt/src/fitcircle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index ef86069c9df..5fa2e028f30 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -76,7 +76,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): :class:`pandas.DataFrame`]) """ - if "L" not in kwargs: + if kwargs.get("L") is None: raise GMTInvalidInput("""Pass a required argument to 'normalize'.""") if output_type not in ["numpy", "pandas", "file"]: raise GMTInvalidInput( @@ -99,7 +99,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): if outfile is None: outfile = tmpfile.name arg_str = " ".join([infile, build_arg_string(kwargs), "->" + outfile]) - lib.call_module("fitcircle", arg_str) + lib.call_module("fitcircle", build_arg_string(kwargs, infile=infile, outfile=outfile) # Read temporary csv output to a pandas table if outfile == tmpfile.name: # if user did not set outfile, return pd.DataFrame From 0c11f2c60e8bfea2c1398ff0f1fa1f9289aaad95 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Thu, 21 Apr 2022 03:39:59 -0600 Subject: [PATCH 17/45] Update pygmt/src/fitcircle.py Co-authored-by: Dongdong Tian --- pygmt/src/fitcircle.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 5fa2e028f30..2a2e050cee1 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -98,7 +98,6 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): with file_context as infile: if outfile is None: outfile = tmpfile.name - arg_str = " ".join([infile, build_arg_string(kwargs), "->" + outfile]) lib.call_module("fitcircle", build_arg_string(kwargs, infile=infile, outfile=outfile) # Read temporary csv output to a pandas table From a6f6265d938189f1558cf439aa9190474e4bd049 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Mon, 2 May 2022 07:08:19 +0100 Subject: [PATCH 18/45] Update pygmt/src/fitcircle.py Co-authored-by: Dongdong Tian --- pygmt/src/fitcircle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 2a2e050cee1..ef8fe9b351b 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -98,7 +98,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): with file_context as infile: if outfile is None: outfile = tmpfile.name - lib.call_module("fitcircle", build_arg_string(kwargs, infile=infile, outfile=outfile) + lib.call_module("fitcircle", build_arg_string(kwargs, infile=infile, outfile=outfile)) # Read temporary csv output to a pandas table if outfile == tmpfile.name: # if user did not set outfile, return pd.DataFrame From bbbb84d636e201c018f6343bb6a1fa5f76d43975 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Mon, 2 May 2022 07:33:24 +0100 Subject: [PATCH 19/45] run make format --- pygmt/__init__.py | 2 +- pygmt/src/fitcircle.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pygmt/__init__.py b/pygmt/__init__.py index 8c1c3636187..ee321b224a0 100644 --- a/pygmt/__init__.py +++ b/pygmt/__init__.py @@ -34,7 +34,7 @@ blockmode, config, dimfilter, - fitcircle, + fitcircle, grd2cpt, grd2xyz, grdclip, diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index ef8fe9b351b..4a53294c142 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -98,7 +98,10 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): with file_context as infile: if outfile is None: outfile = tmpfile.name - lib.call_module("fitcircle", build_arg_string(kwargs, infile=infile, outfile=outfile)) + lib.call_module( + "fitcircle", + build_arg_string(kwargs, infile=infile, outfile=outfile), + ) # Read temporary csv output to a pandas table if outfile == tmpfile.name: # if user did not set outfile, return pd.DataFrame From 00ffffd99add575df0dc2d3f5c870ff11dd43805 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Mon, 2 May 2022 14:37:47 +0100 Subject: [PATCH 20/45] Apply suggestions from code review Co-authored-by: Dongdong Tian --- pygmt/src/fitcircle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 4a53294c142..57dab564773 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -77,10 +77,10 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): """ if kwargs.get("L") is None: - raise GMTInvalidInput("""Pass a required argument to 'normalize'.""") + raise GMTInvalidInput("Pass a required argument to 'normalize'.") if output_type not in ["numpy", "pandas", "file"]: raise GMTInvalidInput( - """Must specify format as either numpy, pandas, or file.""" + "Must specify format as either numpy, pandas, or file." ) if outfile is not None and output_type != "file": msg = ( From af7c4f96e4418b3b9ee08e5b90505353386623f5 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Thu, 5 May 2022 09:52:22 +0100 Subject: [PATCH 21/45] add normalize and small_circle parameters --- pygmt/src/fitcircle.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 57dab564773..4fb5bb7c6fc 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -53,7 +53,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): {aliases} Parameters - ----------. + ---------- output_type : str Determine the format the xyz data will be returned in [Default is ``pandas``]: @@ -63,6 +63,16 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): - ``file`` - ASCII file (requires ``outfile``) outfile : str The file name for the output ASCII file. + normalize : float or bool + Specify the desired *norm* as **1** or **2**\ , or use ``True`` + or **3** to see both solutions. + small_circle : 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]. + {V} Returns ------- @@ -79,9 +89,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): if kwargs.get("L") is None: raise GMTInvalidInput("Pass a required argument to 'normalize'.") if output_type not in ["numpy", "pandas", "file"]: - raise GMTInvalidInput( - "Must specify format as either numpy, pandas, or file." - ) + raise GMTInvalidInput("Must specify format as either numpy, pandas, or file.") if outfile is not None and output_type != "file": msg = ( f"Changing `output_type` of fitcirle from '{output_type}' to 'file' " From d166335db0629e9e44aa4c48d929de7d92750f59 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Fri, 6 May 2022 07:08:18 +0100 Subject: [PATCH 22/45] Apply suggestions from code review Co-authored-by: Dongdong Tian --- pygmt/src/fitcircle.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 4fb5bb7c6fc..4fce1e70ad4 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -1,5 +1,5 @@ """ -fitcircle - Fit coordinators to create vectors on a sphere. +fitcircle - Find mean position and great [or small] circle fit to points on sphere. """ import warnings @@ -17,7 +17,7 @@ ) def fitcircle(data, output_type="pandas", outfile=None, **kwargs): r""" - Fit coordinators to create vectors on a sphere. + Find mean position and great [or small] circle fit to points on sphere. **fitcircle** reads lon,lat [or lat,lon] values from the first two columns on standard input [or *table*]. These are converted to @@ -29,7 +29,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): dispersion, the pole to the great circle will be less well determined than the mean. Compare both solutions as a qualitative check. - Setting `normalize` to **1** approximates the + Setting ``normalize`` 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 @@ -38,7 +38,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): analogous to the "leverage" of distant points in linear regression in the plane. - Setting `normalize` to **2** approximates the + Setting ``normalize`` 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 @@ -99,7 +99,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): warnings.warn(msg, category=RuntimeWarning, stacklevel=2) output_type = "file" elif output_type == "file" and outfile is None: - raise GMTInvalidInput("""Must specify outfile for ASCII output.""") + raise GMTInvalidInput("Must specify outfile for ASCII output.") with GMTTempFile() as tmpfile: with Session() as lib: file_context = lib.virtualfile_from_data(check_kind="vector", data=data) @@ -107,8 +107,8 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): if outfile is None: outfile = tmpfile.name lib.call_module( - "fitcircle", - build_arg_string(kwargs, infile=infile, outfile=outfile), + module="fitcircle", + args=build_arg_string(kwargs, infile=infile, outfile=outfile), ) # Read temporary csv output to a pandas table From 9227dbefda415aa63f2b5b39cf871689d0da8831 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Fri, 6 May 2022 07:10:12 +0100 Subject: [PATCH 23/45] change "normalize" to "norm" --- pygmt/src/fitcircle.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 4fce1e70ad4..bd82a6b4b4a 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -11,7 +11,7 @@ @fmt_docstring @use_alias( - L="normalize", + L="norm", S="small_circle", V="verbose", ) @@ -29,7 +29,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): dispersion, the pole to the great circle will be less well determined than the mean. Compare both solutions as a qualitative check. - Setting ``normalize`` to **1** approximates the + 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 @@ -38,7 +38,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): analogous to the "leverage" of distant points in linear regression in the plane. - Setting ``normalize`` to **2** approximates the + 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 @@ -63,7 +63,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): - ``file`` - ASCII file (requires ``outfile``) outfile : str The file name for the output ASCII file. - normalize : float or bool + norm : float or bool Specify the desired *norm* as **1** or **2**\ , or use ``True`` or **3** to see both solutions. small_circle : float @@ -87,7 +87,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): """ if kwargs.get("L") is None: - raise GMTInvalidInput("Pass a required argument to 'normalize'.") + raise GMTInvalidInput("Pass a required argument to 'norm'.") if output_type not in ["numpy", "pandas", "file"]: raise GMTInvalidInput("Must specify format as either numpy, pandas, or file.") if outfile is not None and output_type != "file": From d75863d35036672cdac6a89461ae23fba6d1964f Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Mon, 23 May 2022 09:27:03 +0100 Subject: [PATCH 24/45] Apply suggestions from code review Co-authored-by: Wei Ji <23487320+weiji14@users.noreply.github.com> --- pygmt/src/fitcircle.py | 4 ++-- pygmt/tests/test_fitcircle.py | 32 ++++++++++++++++---------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index bd82a6b4b4a..d347e966b3a 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -20,7 +20,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): Find mean position and great [or small] circle fit to points on sphere. **fitcircle** reads lon,lat [or lat,lon] values from the first two - columns on standard input [or *table*]. These are converted to + columns of the table. 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 @@ -116,7 +116,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): result = pd.read_csv( tmpfile.name, sep="\t", - names=["longitutde", "latitude", "method"], + names=["longitude", "latitude", "method"], comment=">", ) elif outfile != tmpfile.name: # return None if outfile set, output in outfile diff --git a/pygmt/tests/test_fitcircle.py b/pygmt/tests/test_fitcircle.py index 0da9469cf8d..48692b38cac 100644 --- a/pygmt/tests/test_fitcircle.py +++ b/pygmt/tests/test_fitcircle.py @@ -34,18 +34,18 @@ def test_fitcircle_no_outfile(data): """ Test fitcircle with no set outfile. """ - result = fitcircle(data=data, normalize=True) + result = fitcircle(data=data, norm=True) assert result.shape == (7, 3) # Test longitude results - npt.assert_allclose(result.iloc[:, 0].min(), 52.7434273422) - npt.assert_allclose(result.iloc[:, 0].max(), 330.243649573) - npt.assert_allclose(result.iloc[:, 0].mean(), 223.078116476) - npt.assert_allclose(result.iloc[:, 0].median(), 232.7449849) + npt.assert_allclose(result.longitude.min(), 52.7434273422) + npt.assert_allclose(result.longitude.max(), 330.243649573) + npt.assert_allclose(result.longitude.mean(), 223.078116476) + npt.assert_allclose(result.longitude.median(), 232.7449849) # Test latitude results - npt.assert_allclose(result.iloc[:, 1].min(), -21.2085369093) - npt.assert_allclose(result.iloc[:, 1].max(), 21.2085369093) - npt.assert_allclose(result.iloc[:, 1].mean(), -7.8863683297) - npt.assert_allclose(result.iloc[:, 1].median(), -18.406777) + npt.assert_allclose(result.latitude.min(), -21.2085369093) + npt.assert_allclose(result.latitude.max(), 21.2085369093) + npt.assert_allclose(result.latitude.mean(), -7.8863683297) + npt.assert_allclose(result.latitude.median(), -18.406777) def test_fitcircle_file_output(data): @@ -54,7 +54,7 @@ def test_fitcircle_file_output(data): """ with GMTTempFile(suffix=".txt") as tmpfile: result = fitcircle( - data=data, normalize=True, outfile=tmpfile.name, output_type="file" + data=data, norm=True, outfile=tmpfile.name, output_type="file" ) assert result is None # return value is None assert os.path.exists(path=tmpfile.name) # check that outfile exists @@ -65,7 +65,7 @@ def test_fitcircle_invalid_format(data): Test that fitcircle fails with an incorrect format for output_type. """ with pytest.raises(GMTInvalidInput): - fitcircle(data=data, normalize=True, output_type="a") + fitcircle(data=data, norm=True, output_type="a") def test_fitcircle_no_normalize(data): @@ -82,7 +82,7 @@ def test_fitcircle_no_outfile_specified(data): file name is specified. """ with pytest.raises(GMTInvalidInput): - fitcircle(data=data, normalize=True, output_type="file") + fitcircle(data=data, norm=True, output_type="file") def test_filter1d_outfile_incorrect_output_type(data): @@ -93,7 +93,7 @@ def test_filter1d_outfile_incorrect_output_type(data): with pytest.warns(RuntimeWarning): with GMTTempFile(suffix=".txt") as tmpfile: result = fitcircle( - data=data, normalize=True, outfile=tmpfile.name, output_type="numpy" + data=data, norm=True, outfile=tmpfile.name, output_type="numpy" ) assert result is None # return value is None assert os.path.exists(path=tmpfile.name) # check that outfile exists @@ -103,9 +103,9 @@ def test_fitcircle_format(data): """ Test that correct formats are returned. """ - circle_default = fitcircle(data=data, normalize=True) + circle_default = fitcircle(data=data, norm=True) assert isinstance(circle_default, pd.DataFrame) - circle_array = fitcircle(data=data, normalize=True, output_type="numpy") + circle_array = fitcircle(data=data, norm=True, output_type="numpy") assert isinstance(circle_array, np.ndarray) - circle_df = fitcircle(data=data, normalize=True, output_type="pandas") + circle_df = fitcircle(data=data, norm=True, output_type="pandas") assert isinstance(circle_df, pd.DataFrame) From fa0aa4bf087c2409e64b75c130680636eaeea2d7 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Mon, 23 May 2022 09:33:00 +0100 Subject: [PATCH 25/45] add docstring for "data" --- pygmt/src/fitcircle.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index d347e966b3a..4d24359e873 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -1,5 +1,6 @@ """ -fitcircle - Find mean position and great [or small] circle fit to points on sphere. +fitcircle - Find mean position and great [or small] circle fit to points on +sphere. """ import warnings @@ -54,6 +55,9 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): Parameters ---------- + data : str or list or {table-like} + Pass in either a file name to an ASCII data table, a Python list, a 2D + {table-classes} containing longitude and latitude values. output_type : str Determine the format the xyz data will be returned in [Default is ``pandas``]: From e885b543395df4e36c557dd5b01cb0fa6c072b5e Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Tue, 24 May 2022 09:34:55 +0100 Subject: [PATCH 26/45] Update pygmt/src/fitcircle.py Co-authored-by: Dongdong Tian --- pygmt/src/fitcircle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 4d24359e873..752dff7442f 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -67,7 +67,7 @@ def fitcircle(data, output_type="pandas", outfile=None, **kwargs): - ``file`` - ASCII file (requires ``outfile``) outfile : str The file name for the output ASCII file. - norm : float or bool + norm : int or bool Specify the desired *norm* as **1** or **2**\ , or use ``True`` or **3** to see both solutions. small_circle : float From ea1646db239726492f1a9c419bd089b99d0aebdb Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Sat, 1 Aug 2026 22:08:39 -0400 Subject: [PATCH 27/45] Updates to fitcircle and test_fitcircle --- pygmt/src/fitcircle.py | 169 ++++++++++++++++------------------ pygmt/tests/test_fitcircle.py | 64 +++++++------ 2 files changed, 114 insertions(+), 119 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 752dff7442f..27a13c4b458 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -2,130 +2,121 @@ fitcircle - Find mean position and great [or small] circle fit to points on sphere. """ -import warnings +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 GMTInvalidInput -from pygmt.helpers import GMTTempFile, build_arg_string, fmt_docstring, use_alias +from pygmt.exceptions import GMTParameterError +from pygmt.helpers import ( + build_arg_list, + fmt_docstring, + use_alias, + validate_output_table_type, +) @fmt_docstring @use_alias( L="norm", S="small_circle", - V="verbose", ) -def fitcircle(data, output_type="pandas", outfile=None, **kwargs): +def fitcircle( + data: PathLike | TableLike, + 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""" Find mean position and great [or small] circle fit to points on sphere. - **fitcircle** reads lon,lat [or lat,lon] values from the first two - columns of the table. 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 + **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 **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. + 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 option list at :gmt-docs:`fitcircle.html` + Full GMT docs at :gmt-docs:`fitcircle.html`. - {aliases} + $aliases + - V = verbose Parameters ---------- - data : str or list or {table-like} - Pass in either a file name to an ASCII data table, a Python list, a 2D - {table-classes} containing longitude and latitude values. - output_type : str - Determine the format the xyz data will be returned in [Default is - ``pandas``]: - - - ``numpy`` - :class:`numpy.ndarray` - - ``pandas``- :class:`pandas.DataFrame` - - ``file`` - ASCII file (requires ``outfile``) - outfile : str - The file name for the output ASCII file. + data + Pass in (longitude, latitude) or (latitude, longitude) values by + providing a file name to an ASCII data table, a 2-D + $table_classes. + $output_type + $outfile norm : int or bool - Specify the desired *norm* as **1** or **2**\ , or use ``True`` - or **3** to see both solutions. - small_circle : float + Specify the desired *norm* as **1** or **2**\ , or use ``True`` or + **3** to see both solutions. + 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]. - {V} + $verbose Returns ------- - ret : pandas.DataFrame or numpy.ndarray or None + ret Return type depends on ``outfile`` and ``output_type``: - - None if ``outfile`` is set (output will be stored in file set by + - ``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`` [Default is - :class:`pandas.DataFrame`]) - + - :class:`pandas.DataFrame` or :class:`numpy.ndarray` if ``outfile`` is not set + (depends on ``output_type``) """ if kwargs.get("L") is None: - raise GMTInvalidInput("Pass a required argument to 'norm'.") - if output_type not in ["numpy", "pandas", "file"]: - raise GMTInvalidInput("Must specify format as either numpy, pandas, or file.") - if outfile is not None and output_type != "file": - msg = ( - f"Changing `output_type` of fitcirle from '{output_type}' to 'file' " - "since `outfile` parameter is set. Please use `output_type='file'` " - "to silence this warning." - ) - warnings.warn(msg, category=RuntimeWarning, stacklevel=2) - output_type = "file" - elif output_type == "file" and outfile is None: - raise GMTInvalidInput("Must specify outfile for ASCII output.") - with GMTTempFile() as tmpfile: - with Session() as lib: - file_context = lib.virtualfile_from_data(check_kind="vector", data=data) - with file_context as infile: - if outfile is None: - outfile = tmpfile.name - lib.call_module( - module="fitcircle", - args=build_arg_string(kwargs, infile=infile, outfile=outfile), - ) + raise GMTParameterError(required="norm") - # Read temporary csv output to a pandas table - if outfile == tmpfile.name: # if user did not set outfile, return pd.DataFrame - result = pd.read_csv( - tmpfile.name, - sep="\t", - names=["longitude", "latitude", "method"], - comment=">", - ) - elif outfile != tmpfile.name: # return None if outfile set, output in outfile - result = None + output_type = validate_output_table_type(output_type, outfile=outfile) + + aliasdict = AliasSystem().add_common( + V=verbose, + ) + aliasdict.merge(kwargs) - if output_type == "numpy": - result = result.to_numpy() - return result + 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"], + ) diff --git a/pygmt/tests/test_fitcircle.py b/pygmt/tests/test_fitcircle.py index 48692b38cac..37a94265af0 100644 --- a/pygmt/tests/test_fitcircle.py +++ b/pygmt/tests/test_fitcircle.py @@ -1,15 +1,15 @@ """ -Tests for fitcircle. +Test pygmt.fitcircle. """ -import os +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 GMTInvalidInput +from pygmt.exceptions import GMTParameterError, GMTValueError from pygmt.helpers import GMTTempFile from pygmt.src import which @@ -17,35 +17,28 @@ @pytest.fixture(scope="module", name="data") def fixture_data(): """ - Load the sample data from the sat_03 remote file. + Load the sample data from the @sat_03 remote file. """ fname = which("@sat_03.txt", download="c") - data = pd.read_csv( - fname, - header=None, - skiprows=1, - sep="\t", - names=["longitutde", "latitude", "z"], + return pd.read_csv( + fname, header=None, skiprows=1, sep="\t", names=["longitude", "latitude", "z"] ) - return data +@pytest.mark.benchmark def test_fitcircle_no_outfile(data): """ Test fitcircle with no set outfile. """ result = fitcircle(data=data, norm=True) + assert isinstance(result, pd.DataFrame) assert result.shape == (7, 3) # Test longitude results npt.assert_allclose(result.longitude.min(), 52.7434273422) npt.assert_allclose(result.longitude.max(), 330.243649573) - npt.assert_allclose(result.longitude.mean(), 223.078116476) - npt.assert_allclose(result.longitude.median(), 232.7449849) # Test latitude results npt.assert_allclose(result.latitude.min(), -21.2085369093) npt.assert_allclose(result.latitude.max(), 21.2085369093) - npt.assert_allclose(result.latitude.mean(), -7.8863683297) - npt.assert_allclose(result.latitude.median(), -18.406777) def test_fitcircle_file_output(data): @@ -57,46 +50,47 @@ def test_fitcircle_file_output(data): data=data, norm=True, outfile=tmpfile.name, output_type="file" ) assert result is None # return value is None - assert os.path.exists(path=tmpfile.name) # check that outfile exists + 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(GMTInvalidInput): + with pytest.raises(GMTValueError): fitcircle(data=data, norm=True, output_type="a") -def test_fitcircle_no_normalize(data): +def test_fitcircle_no_norm(data): """ - Test that fitcircle fails with an argument is missing for normalize. + Test that fitcircle fails when the required "norm" parameter is missing. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fitcircle(data=data) def test_fitcircle_no_outfile_specified(data): """ - Test that fitcircle fails when outpput_type is set to 'file' but no output - file name is specified. + Test that fitcircle fails when output_type is set to "file" but no outfile + is specified. """ - with pytest.raises(GMTInvalidInput): + with pytest.raises(GMTParameterError): fitcircle(data=data, norm=True, output_type="file") -def test_filter1d_outfile_incorrect_output_type(data): +def test_fitcircle_outfile_incorrect_output_type(data): """ - Test that filter1d raises a warning when an outfile filename is set but the - output_type is not set to 'file'. + Test that fitcircle raises a warning when an outfile filename is set but the + output_type is not set to "file". """ - with pytest.warns(RuntimeWarning): - with GMTTempFile(suffix=".txt") as tmpfile: + 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 result is None # return value is None - assert os.path.exists(path=tmpfile.name) # check that outfile exists + 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): @@ -109,3 +103,13 @@ def test_fitcircle_format(data): assert isinstance(circle_array, np.ndarray) circle_df = fitcircle(data=data, norm=True, output_type="pandas") assert isinstance(circle_df, pd.DataFrame) + + +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] From 8bcc2ffa73b015fa9c67b9213a46bdfc7df460c2 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Mon, 3 Aug 2026 19:07:41 -0400 Subject: [PATCH 28/45] Add suggested fixes and test --- pygmt/helpers/caching.py | 1 + pygmt/src/fitcircle.py | 17 +++++++++++++++-- pygmt/tests/test_fitcircle.py | 31 +++++++++++++++++++++++-------- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/pygmt/helpers/caching.py b/pygmt/helpers/caching.py index e315a0eddcc..51c886f4fee 100644 --- a/pygmt/helpers/caching.py +++ b/pygmt/helpers/caching.py @@ -116,6 +116,7 @@ def cache_data() -> None: "@RidgeTest.prj", "@RidgeTest.shp", "@RidgeTest.shx", + "@sat_03.txt", "@SOEST_block4.png", "@Table_5_11.txt", "@Table_5_11_mean.xyz", diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 27a13c4b458..16d81b81bda 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -10,7 +10,7 @@ from pygmt._typing import PathLike, TableLike from pygmt.alias import AliasSystem from pygmt.clib import Session -from pygmt.exceptions import GMTParameterError +from pygmt.exceptions import GMTParameterError, GMTValueError from pygmt.helpers import ( build_arg_list, fmt_docstring, @@ -77,7 +77,9 @@ def fitcircle( $outfile norm : int or bool Specify the desired *norm* as **1** or **2**\ , or use ``True`` or - **3** to see both solutions. + **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 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 @@ -100,6 +102,17 @@ def fitcircle( 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, diff --git a/pygmt/tests/test_fitcircle.py b/pygmt/tests/test_fitcircle.py index 37a94265af0..bd7a0356bbe 100644 --- a/pygmt/tests/test_fitcircle.py +++ b/pygmt/tests/test_fitcircle.py @@ -30,15 +30,15 @@ def test_fitcircle_no_outfile(data): """ Test fitcircle with no set outfile. """ - result = fitcircle(data=data, norm=True) + result = fitcircle(data=data, norm=2) assert isinstance(result, pd.DataFrame) - assert result.shape == (7, 3) + assert result.shape == (4, 3) # Test longitude results - npt.assert_allclose(result.longitude.min(), 52.7434273422) + 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.2085369093) - npt.assert_allclose(result.latitude.max(), 21.2085369093) + npt.assert_allclose(result.latitude.min(), -21.2046833116) + npt.assert_allclose(result.latitude.max(), 21.2046833116) def test_fitcircle_file_output(data): @@ -97,14 +97,29 @@ def test_fitcircle_format(data): """ Test that correct formats are returned. """ - circle_default = fitcircle(data=data, norm=True) + circle_default = fitcircle(data=data, norm=2) assert isinstance(circle_default, pd.DataFrame) - circle_array = fitcircle(data=data, norm=True, output_type="numpy") + circle_array = fitcircle(data=data, norm=2, output_type="numpy") assert isinstance(circle_array, np.ndarray) - circle_df = fitcircle(data=data, norm=True, output_type="pandas") + 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. From 7cb916ce11cdb411234f4601e974494b871dfc27 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Wed, 5 Aug 2026 20:15:58 -0400 Subject: [PATCH 29/45] Make suggested changed for alias system and parameters --- pygmt/src/fitcircle.py | 88 +++++++++++++++++++---------------- pygmt/tests/test_fitcircle.py | 40 +++++++++------- 2 files changed, 71 insertions(+), 57 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 16d81b81bda..c451d15d4c6 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -8,26 +8,21 @@ import numpy as np import pandas as pd from pygmt._typing import PathLike, TableLike -from pygmt.alias import AliasSystem +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, - use_alias, - validate_output_table_type, -) +from pygmt.helpers import build_arg_list, fmt_docstring, validate_output_table_type @fmt_docstring -@use_alias( - L="norm", - S="small_circle", -) def fitcircle( - data: PathLike | TableLike, + 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, @@ -45,22 +40,26 @@ def fitcircle( 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. + 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`. @@ -73,13 +72,16 @@ def fitcircle( 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 : int or bool - 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. + 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 @@ -98,30 +100,34 @@ def fitcircle( - :class:`pandas.DataFrame` or :class:`numpy.ndarray` if ``outfile`` is not set (depends on ``output_type``) """ - if kwargs.get("L") is None: + if norm 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): + 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 True or 3 " - "since both L1 and L2 solutions are stacked in the same rows. " - "Use output_type='numpy' or output_type='file' instead." + "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().add_common( + 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) as vintbl, + 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( diff --git a/pygmt/tests/test_fitcircle.py b/pygmt/tests/test_fitcircle.py index bd7a0356bbe..b4cbd41807b 100644 --- a/pygmt/tests/test_fitcircle.py +++ b/pygmt/tests/test_fitcircle.py @@ -30,7 +30,7 @@ def test_fitcircle_no_outfile(data): """ Test fitcircle with no set outfile. """ - result = fitcircle(data=data, norm=2) + result = fitcircle(data=data, norm="squares") assert isinstance(result, pd.DataFrame) assert result.shape == (4, 3) # Test longitude results @@ -47,7 +47,7 @@ def test_fitcircle_file_output(data): """ with GMTTempFile(suffix=".txt") as tmpfile: result = fitcircle( - data=data, norm=True, outfile=tmpfile.name, output_type="file" + 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 @@ -58,7 +58,7 @@ 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") + fitcircle(data=data, norm="both", output_type="a") def test_fitcircle_no_norm(data): @@ -75,7 +75,7 @@ def test_fitcircle_no_outfile_specified(data): is specified. """ with pytest.raises(GMTParameterError): - fitcircle(data=data, norm=True, output_type="file") + fitcircle(data=data, norm="both", output_type="file") def test_fitcircle_outfile_incorrect_output_type(data): @@ -86,7 +86,7 @@ def test_fitcircle_outfile_incorrect_output_type(data): with GMTTempFile(suffix=".txt") as tmpfile: with pytest.warns(RuntimeWarning) as record: result = fitcircle( - data=data, norm=True, outfile=tmpfile.name, output_type="numpy" + 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 @@ -97,26 +97,25 @@ def test_fitcircle_format(data): """ Test that correct formats are returned. """ - circle_default = fitcircle(data=data, norm=2) + circle_default = fitcircle(data=data, norm="squares") assert isinstance(circle_default, pd.DataFrame) - circle_array = fitcircle(data=data, norm=2, output_type="numpy") + circle_array = fitcircle(data=data, norm="squares", output_type="numpy") assert isinstance(circle_array, np.ndarray) - circle_df = fitcircle(data=data, norm=2, output_type="pandas") + circle_df = fitcircle(data=data, norm="squares", 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): +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 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. + 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=norm) + fitcircle(data=data, norm="both") with pytest.raises(GMTValueError): - fitcircle(data=data, norm=norm, output_type="pandas") - result = fitcircle(data=data, norm=norm, output_type="numpy") + fitcircle(data=data, norm="both", output_type="pandas") + result = fitcircle(data=data, norm="both", output_type="numpy") assert isinstance(result, np.ndarray) @@ -124,7 +123,16 @@ 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) + 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) From 6f0e501c0e9e0acc88d89bc7cc706ca540e24a69 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Thu, 13 Aug 2026 22:41:35 -0400 Subject: [PATCH 30/45] Modify to return dictionary --- pygmt/src/fitcircle.py | 110 ++++++++++++++++------------ pygmt/tests/test_fitcircle.py | 132 ++++++++++++---------------------- 2 files changed, 109 insertions(+), 133 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index c451d15d4c6..cc6b7f4888c 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -1,17 +1,16 @@ """ -fitcircle - Find mean position and great [or small] circle fit to points on +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 +from pygmt.exceptions import GMTParameterError +from pygmt.helpers import build_arg_list, fmt_docstring +from pygmt.helpers.utils import is_given @fmt_docstring @@ -19,26 +18,21 @@ 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, + norm: Literal["absolutes", "squares"] | None = None, small_circle: bool | float = False, verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, **kwargs, -) -> pd.DataFrame | np.ndarray | None: +) -> dict[str, tuple[float, float] | float] | None: r""" - Find mean position and great [or small] circle fit to points on sphere. + 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. + the input positions. Setting ``norm`` to ``"absolutes"`` approximates the minimization of the sum of absolute values of cosines of angular distances. This solution @@ -57,6 +51,12 @@ def fitcircle( smallest eigenvalue; it is the least-well represented factor in the data and is not easily estimated by either method. + 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 by calling :func:`pygmt.fitcircle` + twice, once for each ``norm``. + Takes a matrix, (x, y) pairs, or a file name as input. Must provide either ``data`` or ``x`` and ``y``. @@ -74,14 +74,11 @@ def fitcircle( $table_classes. x/y : 1-D arrays Arrays of x and y coordinates of the data points. - $output_type - $outfile + outfile + The file name for the full GMT text report. If set, no ``dict`` is + returned; the raw GMT report is written to ``outfile`` instead. 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. + Specify the desired norm, either ``"absolutes"`` or ``"squares"``. 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 @@ -93,49 +90,70 @@ def fitcircle( 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``) + ``None`` if ``outfile`` is set (the raw GMT report is written to + ``outfile`` instead). Otherwise, a ``dict`` with the following keys, + each mapping to a ``(longitude, latitude)`` tuple: + + - ``"flat_mean"``: the flat Earth mean position + - ``"mean"``: the mean position (Fisher or eigenvalue method, + depending on ``norm``) + - ``"north_pole"``: the north hemisphere great circle pole + - ``"south_pole"``: the south hemisphere great circle pole + + If ``small_circle`` is set, two more keys are added: + + - ``"small_circle_pole"``: the small circle pole + - ``"small_circle_distance"``: the colatitude/distance in degrees + from the small circle pole to the small circle (a ``float``, not a + tuple) """ 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}), + L=Alias(norm, name="norm", mapping={"absolutes": 1, "squares": 2}), S=Alias(small_circle, name="small_circle"), ).add_common( V=verbose, ) aliasdict.merge(kwargs) + if outfile is not None: + with Session() as lib: + with lib.virtualfile_in( + check_kind="vector", data=data, x=x, y=y, mincols=2 + ) as vintbl: + lib.call_module( + module="fitcircle", + args=build_arg_list(aliasdict, infile=vintbl, outfile=outfile), + ) + return None + + # "c" (small-circle pole and colatitude) is only valid with -S; GMT errors + # ("Cannot select c without setting -S") if "c" is requested without it. + aliasdict["F"] = "fmnsc" if is_given(small_circle) else "fmns" + 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.virtualfile_out(kind="dataset") 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"], - ) + row = lib.virtualfile_to_dataset(vfname=vouttbl, output_type="numpy")[0] + values = [float(value) for value in row] + + solution = { + "flat_mean": (values[0], values[1]), + "mean": (values[2], values[3]), + "north_pole": (values[4], values[5]), + "south_pole": (values[6], values[7]), + } + if is_given(small_circle): + solution["small_circle_pole"] = (values[8], values[9]) + solution["small_circle_distance"] = values[10] + return solution diff --git a/pygmt/tests/test_fitcircle.py b/pygmt/tests/test_fitcircle.py index b4cbd41807b..e8446c1ac54 100644 --- a/pygmt/tests/test_fitcircle.py +++ b/pygmt/tests/test_fitcircle.py @@ -4,12 +4,11 @@ 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.exceptions import GMTParameterError from pygmt.helpers import GMTTempFile from pygmt.src import which @@ -26,113 +25,72 @@ def fixture_data(): @pytest.mark.benchmark -def test_fitcircle_no_outfile(data): +def test_fitcircle_absolutes(data): """ - Test fitcircle with no set outfile. + Test fitcircle with norm="absolutes". """ - 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 + result = fitcircle(data=data, norm="absolutes") + assert isinstance(result, dict) + assert set(result.keys()) == {"flat_mean", "mean", "north_pole", "south_pole"} + npt.assert_allclose(result["flat_mean"], (330.243649573, -18.3910128205)) + npt.assert_allclose(result["mean"], (330.16313328, -18.4067771888)) + npt.assert_allclose(result["north_pole"], (52.7434273422, 21.2085369093)) + npt.assert_allclose(result["south_pole"], (232.743427342, -21.2085369093)) -def test_fitcircle_invalid_format(data): +def test_fitcircle_squares(data): """ - Test that fitcircle fails with an incorrect format for output_type. + Test fitcircle with norm="squares". """ - with pytest.raises(GMTValueError): - fitcircle(data=data, norm="both", output_type="a") + result = fitcircle(data=data, norm="squares") + assert isinstance(result, dict) + assert set(result.keys()) == {"flat_mean", "mean", "north_pole", "south_pole"} + npt.assert_allclose(result["flat_mean"], (330.243649573, -18.3910128205)) + npt.assert_allclose(result["mean"], (330.163207808, -18.4067882988)) + npt.assert_allclose(result["north_pole"], (52.7449849947, 21.2046833116)) + npt.assert_allclose(result["south_pole"], (232.744984995, -21.2046833116)) -def test_fitcircle_no_norm(data): +def test_fitcircle_small_circle(data): """ - Test that fitcircle fails when the required "norm" parameter is missing. + Test that fitcircle can fit a small circle instead of a great circle, and + that the returned dict includes the small-circle keys. """ - with pytest.raises(GMTParameterError): - fitcircle(data=data) + result = fitcircle(data=data, norm="squares", small_circle=True) + assert isinstance(result, dict) + assert set(result.keys()) == { + "flat_mean", + "mean", + "north_pole", + "south_pole", + "small_circle_pole", + "small_circle_distance", + } + npt.assert_allclose(result["small_circle_distance"], 87.6072781238) -def test_fitcircle_no_outfile_specified(data): +def test_fitcircle_input_xy(data): """ - Test that fitcircle fails when output_type is set to "file" but no outfile - is specified. + Run fitcircle by passing in x/y as input. """ - with pytest.raises(GMTParameterError): - fitcircle(data=data, norm="both", output_type="file") + result = fitcircle(x=data.longitude, y=data.latitude, norm="absolutes") + assert isinstance(result, dict) + npt.assert_allclose(result["flat_mean"], (330.243649573, -18.3910128205)) -def test_fitcircle_outfile_incorrect_output_type(data): +def test_fitcircle_outfile(data): """ - Test that fitcircle raises a warning when an outfile filename is set but the - output_type is not set to "file". + Test that fitcircle returns None and writes a file when outfile is set. """ 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 + result = fitcircle(data=data, norm="squares", outfile=tmpfile.name) 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): +def test_fitcircle_no_norm(data): """ - Run fitcircle by passing in x/y as input. + Test that fitcircle fails when the required "norm" parameter is missing. """ - result = fitcircle(x=data.longitude, y=data.latitude, norm="squares") - assert isinstance(result, pd.DataFrame) - assert result.shape == (4, 3) + with pytest.raises(GMTParameterError): + fitcircle(data=data) From eff642e2f3edecd0e4ccd802d35b928d69d2a739 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Thu, 13 Aug 2026 23:31:07 -0400 Subject: [PATCH 31/45] Add typehint --- pygmt/src/fitcircle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index cc6b7f4888c..388f0db90be 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -147,7 +147,7 @@ def fitcircle( row = lib.virtualfile_to_dataset(vfname=vouttbl, output_type="numpy")[0] values = [float(value) for value in row] - solution = { + solution: dict[str, tuple[float, float] | float] = { "flat_mean": (values[0], values[1]), "mean": (values[2], values[3]), "north_pole": (values[4], values[5]), From 4cd670206a431c3ecd4e0c6319fd503c0acd6287 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Fri, 14 Aug 2026 15:49:29 -0400 Subject: [PATCH 32/45] Update pygmt/src/fitcircle.py Co-authored-by: Dongdong Tian --- pygmt/src/fitcircle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 388f0db90be..80f7ee99771 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -79,7 +79,7 @@ def fitcircle( returned; the raw GMT report is written to ``outfile`` instead. norm Specify the desired norm, either ``"absolutes"`` or ``"squares"``. - small_circle : bool or float + small_circle 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. From ff4c77ce1c2792c9b6744761fae20f1e24f011b7 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Fri, 14 Aug 2026 15:49:36 -0400 Subject: [PATCH 33/45] Update pygmt/src/fitcircle.py Co-authored-by: Dongdong Tian --- pygmt/src/fitcircle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 80f7ee99771..ad3e9f4f75e 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -69,7 +69,7 @@ def fitcircle( Parameters ---------- data - Pass in (longitude, latitude) or (latitude, longitude) values by + Pass in (longitude, latitude) values by providing a file name to an ASCII data table, a 2-D $table_classes. x/y : 1-D arrays From 3be5bace36c484d8a8deab0f053b81153ce69bf0 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Fri, 14 Aug 2026 15:49:44 -0400 Subject: [PATCH 34/45] Update pygmt/src/fitcircle.py Co-authored-by: Dongdong Tian --- pygmt/src/fitcircle.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index ad3e9f4f75e..b20908dbfda 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -1,6 +1,5 @@ """ -fitcircle - Find mean position and great or small circle fit to points on -sphere. +fitcircle - Find mean position and great or small circle fit to points on sphere. """ from typing import Literal From a6777d8c811761f2b9df96039b763345ca5c735f Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Fri, 14 Aug 2026 15:53:23 -0400 Subject: [PATCH 35/45] Update pygmt/src/fitcircle.py Co-authored-by: Dongdong Tian --- pygmt/src/fitcircle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index b20908dbfda..99cb7b6c73e 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -24,7 +24,7 @@ def fitcircle( | bool = False, **kwargs, ) -> dict[str, tuple[float, float] | float] | 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 From 6bd302ac8cfaa2347d0b8daa07fd49707545abc1 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Fri, 14 Aug 2026 15:53:30 -0400 Subject: [PATCH 36/45] Update pygmt/src/fitcircle.py Co-authored-by: Dongdong Tian --- pygmt/src/fitcircle.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 99cb7b6c73e..cdb16887044 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -27,8 +27,7 @@ def fitcircle( """ 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 + This method takes (longitude, latitude) values and converts them 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. From cdf4ef9a6b8dee4367853c4145c5d85c58af7efe Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Fri, 14 Aug 2026 16:02:05 -0400 Subject: [PATCH 37/45] remote outfile option from fitcircle --- pygmt/src/fitcircle.py | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index cdb16887044..4d2caa4ac27 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -17,13 +17,12 @@ def fitcircle( data: PathLike | TableLike | None = None, x=None, y=None, - outfile: PathLike | None = None, norm: Literal["absolutes", "squares"] | None = None, small_circle: bool | float = False, verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, **kwargs, -) -> dict[str, tuple[float, float] | float] | None: +) -> dict[str, tuple[float, float] | float]: """ Find mean position and great or small circle fit to points on sphere. @@ -72,9 +71,6 @@ def fitcircle( $table_classes. x/y : 1-D arrays Arrays of x and y coordinates of the data points. - outfile - The file name for the full GMT text report. If set, no ``dict`` is - returned; the raw GMT report is written to ``outfile`` instead. norm Specify the desired norm, either ``"absolutes"`` or ``"squares"``. small_circle @@ -88,9 +84,8 @@ def fitcircle( Returns ------- ret - ``None`` if ``outfile`` is set (the raw GMT report is written to - ``outfile`` instead). Otherwise, a ``dict`` with the following keys, - each mapping to a ``(longitude, latitude)`` tuple: + A ``dict`` with the following keys, each mapping to a + ``(longitude, latitude)`` tuple: - ``"flat_mean"``: the flat Earth mean position - ``"mean"``: the mean position (Fisher or eigenvalue method, @@ -116,17 +111,6 @@ def fitcircle( ) aliasdict.merge(kwargs) - if outfile is not None: - with Session() as lib: - with lib.virtualfile_in( - check_kind="vector", data=data, x=x, y=y, mincols=2 - ) as vintbl: - lib.call_module( - module="fitcircle", - args=build_arg_list(aliasdict, infile=vintbl, outfile=outfile), - ) - return None - # "c" (small-circle pole and colatitude) is only valid with -S; GMT errors # ("Cannot select c without setting -S") if "c" is requested without it. aliasdict["F"] = "fmnsc" if is_given(small_circle) else "fmns" From 59afc1f4f3d7692b9f5690b587954e39dd918cbb Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Fri, 14 Aug 2026 16:04:25 -0400 Subject: [PATCH 38/45] Update tests to remove testing for outfile --- pygmt/tests/test_fitcircle.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/pygmt/tests/test_fitcircle.py b/pygmt/tests/test_fitcircle.py index e8446c1ac54..d22dc9ec99f 100644 --- a/pygmt/tests/test_fitcircle.py +++ b/pygmt/tests/test_fitcircle.py @@ -2,14 +2,11 @@ Test pygmt.fitcircle. """ -from pathlib import Path - import numpy.testing as npt import pandas as pd import pytest from pygmt import fitcircle from pygmt.exceptions import GMTParameterError -from pygmt.helpers import GMTTempFile from pygmt.src import which @@ -78,16 +75,6 @@ def test_fitcircle_input_xy(data): npt.assert_allclose(result["flat_mean"], (330.243649573, -18.3910128205)) -def test_fitcircle_outfile(data): - """ - Test that fitcircle returns None and writes a file when outfile is set. - """ - with GMTTempFile(suffix=".txt") as tmpfile: - result = fitcircle(data=data, norm="squares", outfile=tmpfile.name) - assert result is None # return value is None - assert Path(tmpfile.name).stat().st_size > 0 # check that outfile exists - - def test_fitcircle_no_norm(data): """ Test that fitcircle fails when the required "norm" parameter is missing. From ec5deec0acf71c9be58f79eeb0ddd5d78a536f0a Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Fri, 14 Aug 2026 16:21:52 -0400 Subject: [PATCH 39/45] Add default value for norm --- pygmt/src/fitcircle.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 4d2caa4ac27..3e8bd14b667 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -7,7 +7,6 @@ from pygmt._typing import PathLike, TableLike from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session -from pygmt.exceptions import GMTParameterError from pygmt.helpers import build_arg_list, fmt_docstring from pygmt.helpers.utils import is_given @@ -17,7 +16,7 @@ def fitcircle( data: PathLike | TableLike | None = None, x=None, y=None, - norm: Literal["absolutes", "squares"] | None = None, + norm: Literal["absolutes", "squares"] = "squares", small_circle: bool | float = False, verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, @@ -72,7 +71,8 @@ def fitcircle( x/y : 1-D arrays Arrays of x and y coordinates of the data points. norm - Specify the desired norm, either ``"absolutes"`` or ``"squares"``. + Specify the desired norm, either ``"absolutes"`` or ``"squares"`` + [Default is ``"squares"``]. small_circle 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 @@ -100,9 +100,6 @@ def fitcircle( from the small circle pole to the small circle (a ``float``, not a tuple) """ - if norm is None: - raise GMTParameterError(required="norm") - aliasdict = AliasSystem( L=Alias(norm, name="norm", mapping={"absolutes": 1, "squares": 2}), S=Alias(small_circle, name="small_circle"), From c0d0423df783f8fa69d7bb5b02065d94a0c37a86 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Fri, 14 Aug 2026 16:22:36 -0400 Subject: [PATCH 40/45] Update test_fitcircle to remove outfile and set default for norm --- pygmt/tests/test_fitcircle.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pygmt/tests/test_fitcircle.py b/pygmt/tests/test_fitcircle.py index d22dc9ec99f..b9ab3f64c81 100644 --- a/pygmt/tests/test_fitcircle.py +++ b/pygmt/tests/test_fitcircle.py @@ -6,7 +6,6 @@ import pandas as pd import pytest from pygmt import fitcircle -from pygmt.exceptions import GMTParameterError from pygmt.src import which @@ -37,7 +36,7 @@ def test_fitcircle_absolutes(data): def test_fitcircle_squares(data): """ - Test fitcircle with norm="squares". + Test fitcircle with norm="squares", which is also the default. """ result = fitcircle(data=data, norm="squares") assert isinstance(result, dict) @@ -46,6 +45,7 @@ def test_fitcircle_squares(data): npt.assert_allclose(result["mean"], (330.163207808, -18.4067882988)) npt.assert_allclose(result["north_pole"], (52.7449849947, 21.2046833116)) npt.assert_allclose(result["south_pole"], (232.744984995, -21.2046833116)) + assert fitcircle(data=data) == result # norm="squares" is the default def test_fitcircle_small_circle(data): @@ -73,11 +73,3 @@ def test_fitcircle_input_xy(data): result = fitcircle(x=data.longitude, y=data.latitude, norm="absolutes") assert isinstance(result, dict) npt.assert_allclose(result["flat_mean"], (330.243649573, -18.3910128205)) - - -def test_fitcircle_no_norm(data): - """ - Test that fitcircle fails when the required "norm" parameter is missing. - """ - with pytest.raises(GMTParameterError): - fitcircle(data=data) From 696a96d655a5e63e5f9b7128f4126d38b9615be6 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Sat, 15 Aug 2026 17:55:41 -0400 Subject: [PATCH 41/45] Update pygmt/src/fitcircle.py Co-authored-by: Dongdong Tian --- pygmt/src/fitcircle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 3e8bd14b667..a6aa0fe9b68 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -84,7 +84,7 @@ def fitcircle( Returns ------- ret - A ``dict`` with the following keys, each mapping to a + A dictionary with the following keys, each mapping to a ``(longitude, latitude)`` tuple: - ``"flat_mean"``: the flat Earth mean position From 5856e64a0e28029d26e260de04b4063487f1f1ee Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Sat, 15 Aug 2026 17:55:53 -0400 Subject: [PATCH 42/45] Update pygmt/src/fitcircle.py Co-authored-by: Dongdong Tian --- pygmt/src/fitcircle.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index a6aa0fe9b68..0d05c3ad5a5 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -74,11 +74,10 @@ def fitcircle( Specify the desired norm, either ``"absolutes"`` or ``"squares"`` [Default is ``"squares"``]. small_circle - 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]. + 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 set the desired fixed + latitude of the small circle [Default will determine the optimal latitude]. $verbose Returns From dd714ee4f754b52d9bb0d022c3213b9e51cd9838 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Sat, 15 Aug 2026 17:56:05 -0400 Subject: [PATCH 43/45] Update pygmt/src/fitcircle.py Co-authored-by: Dongdong Tian --- pygmt/src/fitcircle.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 0d05c3ad5a5..60105df9c23 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -65,9 +65,8 @@ def fitcircle( Parameters ---------- data - Pass in (longitude, latitude) values by - providing a file name to an ASCII data table, a 2-D - $table_classes. + Pass in (longitude, latitude) 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. norm From 961ad92a18909ff9402d8156e8402d7b79e557f9 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Sat, 15 Aug 2026 17:56:19 -0400 Subject: [PATCH 44/45] Update pygmt/src/fitcircle.py Co-authored-by: Dongdong Tian --- pygmt/src/fitcircle.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 60105df9c23..26275be7eec 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -59,7 +59,13 @@ def fitcircle( Full GMT docs at :gmt-docs:`fitcircle.html`. - $aliases + **Aliases:** + + .. hlist:: + :columns: 3 + + - L = norm + - S = small_circle - V = verbose Parameters From efbe10d05f910fb55d9bd76a4e91a0ea94ba0be5 Mon Sep 17 00:00:00 2001 From: Will Schlitzer Date: Sat, 15 Aug 2026 17:59:58 -0400 Subject: [PATCH 45/45] Formatting fix --- pygmt/src/fitcircle.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pygmt/src/fitcircle.py b/pygmt/src/fitcircle.py index 26275be7eec..3cdf8b29159 100644 --- a/pygmt/src/fitcircle.py +++ b/pygmt/src/fitcircle.py @@ -60,12 +60,12 @@ def fitcircle( Full GMT docs at :gmt-docs:`fitcircle.html`. **Aliases:** - + .. hlist:: :columns: 3 - + - L = norm - - S = small_circle + - S = small_circle - V = verbose Parameters @@ -79,7 +79,7 @@ def fitcircle( Specify the desired norm, either ``"absolutes"`` or ``"squares"`` [Default is ``"squares"``]. small_circle - Attempt to fit a small circle instead of a great circle. The pole will be + 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 set the desired fixed latitude of the small circle [Default will determine the optimal latitude].