Skip to content
Open
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
7c1ae68
add fitcircle.py
willschlitzer Sep 26, 2021
41a3ded
add fitcircle imports
willschlitzer Sep 28, 2021
7e2dfee
add test_fitcircle
willschlitzer Sep 28, 2021
a239841
add if statements and df column names
willschlitzer Sep 28, 2021
d526eab
update formatting in fixture_data
willschlitzer Sep 28, 2021
f02466e
formatting
willschlitzer Sep 29, 2021
0355a7d
add functions
willschlitzer Sep 29, 2021
9bf8331
add test info to test_fitcircle_no_outfile
willschlitzer Sep 29, 2021
0360abc
add if statement for normalize
willschlitzer Sep 29, 2021
1e86759
add tests
willschlitzer Sep 29, 2021
e5b7a91
add fitcircle to index.rst
willschlitzer Nov 6, 2021
3cde6d8
run make format
willschlitzer Nov 6, 2021
c199d09
remove unused imports
willschlitzer Nov 6, 2021
899a93f
Merge branch 'main' into wrap/fitcircle
willschlitzer Nov 6, 2021
d131d2f
change top docstring
willschlitzer Nov 6, 2021
3e57da6
Merge remote-tracking branch 'origin/wrap/fitcircle' into wrap/fitcircle
willschlitzer Nov 6, 2021
d4eebb7
fix variable names
willschlitzer Jan 14, 2022
913a70b
Merge branch 'main' into wrap/fitcircle
willschlitzer Jan 14, 2022
f0033fa
Merge branch 'main' into wrap/fitcircle
willschlitzer Apr 13, 2022
d41d90e
Apply suggestions from code review
willschlitzer Apr 19, 2022
0c11f2c
Update pygmt/src/fitcircle.py
willschlitzer Apr 21, 2022
a6f6265
Update pygmt/src/fitcircle.py
willschlitzer May 2, 2022
8ab458a
Merge branch 'main' into wrap/fitcircle
willschlitzer May 2, 2022
bbbb84d
run make format
willschlitzer May 2, 2022
00ffffd
Apply suggestions from code review
willschlitzer May 2, 2022
af7c4f9
add normalize and small_circle parameters
willschlitzer May 5, 2022
8803d43
Merge branch 'main' into wrap/fitcircle
willschlitzer May 5, 2022
d166335
Apply suggestions from code review
willschlitzer May 6, 2022
9227dbe
change "normalize" to "norm"
willschlitzer May 6, 2022
a10ed09
Merge branch 'main' into wrap/fitcircle
willschlitzer May 6, 2022
d75863d
Apply suggestions from code review
willschlitzer May 23, 2022
bfa8fa6
Merge branch 'main' into wrap/fitcircle
willschlitzer May 23, 2022
fa0aa4b
add docstring for "data"
willschlitzer May 23, 2022
e885b54
Update pygmt/src/fitcircle.py
willschlitzer May 24, 2022
c890477
Merge branch 'main' into wrap/fitcircle
willschlitzer Dec 1, 2022
bc1017a
Merge branch 'main' into wrap/fitcircle
willschlitzer Aug 2, 2026
ea1646d
Updates to fitcircle and test_fitcircle
willschlitzer Aug 2, 2026
8bcc2ff
Add suggested fixes and test
willschlitzer Aug 3, 2026
68a12b0
Merge branch 'main' into wrap/fitcircle
willschlitzer Aug 3, 2026
7cb916c
Make suggested changed for alias system and parameters
willschlitzer Aug 6, 2026
6f0e501
Modify to return dictionary
willschlitzer Aug 14, 2026
0e196f8
Merge branch 'main' into wrap/fitcircle
willschlitzer Aug 14, 2026
eff642e
Add typehint
willschlitzer Aug 14, 2026
4cd6702
Update pygmt/src/fitcircle.py
willschlitzer Aug 14, 2026
ff4c77c
Update pygmt/src/fitcircle.py
willschlitzer Aug 14, 2026
3be5bac
Update pygmt/src/fitcircle.py
willschlitzer Aug 14, 2026
5fdfede
Merge branch 'main' into wrap/fitcircle
willschlitzer Aug 14, 2026
a6777d8
Update pygmt/src/fitcircle.py
willschlitzer Aug 14, 2026
6bd302a
Update pygmt/src/fitcircle.py
willschlitzer Aug 14, 2026
cdf4ef9
remote outfile option from fitcircle
Aug 14, 2026
59afc1f
Update tests to remove testing for outfile
willschlitzer Aug 14, 2026
ec5deec
Add default value for norm
willschlitzer Aug 14, 2026
c0d0423
Update test_fitcircle to remove outfile and set default for norm
willschlitzer Aug 14, 2026
696a96d
Update pygmt/src/fitcircle.py
willschlitzer Aug 15, 2026
5856e64
Update pygmt/src/fitcircle.py
willschlitzer Aug 15, 2026
dd714ee
Update pygmt/src/fitcircle.py
willschlitzer Aug 15, 2026
961ad92
Update pygmt/src/fitcircle.py
willschlitzer Aug 15, 2026
efbe10d
Formatting fix
willschlitzer Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ Operations on tabular data
blockmedian
blockmode
filter1d
fitcircle
nearneighbor
project
select
Expand Down
1 change: 1 addition & 0 deletions pygmt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
config,
dimfilter,
filter1d,
fitcircle,
grd2cpt,
grd2xyz,
grdclip,
Expand Down
1 change: 1 addition & 0 deletions pygmt/helpers/caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ def cache_data() -> None:
"@RidgeTest.prj",
"@RidgeTest.shp",
"@RidgeTest.shx",
"@sat_03.txt",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this file to Line 130.

"@SOEST_block4.png",
"@Table_5_11.txt",
"@Table_5_11_mean.xyz",
Expand Down
1 change: 1 addition & 0 deletions pygmt/src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pygmt.src.config import config
from pygmt.src.dimfilter import dimfilter
from pygmt.src.filter1d import filter1d
from pygmt.src.fitcircle import fitcircle
from pygmt.src.grd2cpt import grd2cpt
from pygmt.src.grd2xyz import grd2xyz
from pygmt.src.grdclip import grdclip
Expand Down
135 changes: 135 additions & 0 deletions pygmt/src/fitcircle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""
fitcircle - Find mean position and great [or small] circle fit to points on
sphere.
"""

from typing import Literal

import numpy as np
import pandas as pd
from pygmt._typing import PathLike, TableLike
from pygmt.alias import AliasSystem
from pygmt.clib import Session
from pygmt.exceptions import GMTParameterError, GMTValueError
from pygmt.helpers import (
build_arg_list,
fmt_docstring,
use_alias,
validate_output_table_type,
)


@fmt_docstring
@use_alias(
L="norm",
S="small_circle",
)
Comment thread
seisman marked this conversation as resolved.
Outdated
def fitcircle(
data: PathLike | TableLike,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should support parameters x/y, too.

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"""
Comment thread
willschlitzer marked this conversation as resolved.
Outdated
Find mean position and great [or small] circle fit to points on sphere.

**fitcircle** reads (longitude, latitude) or (latitude, longitude) values from the
first two columns of the input data. These are converted to Cartesian
three-vectors on the unit sphere. Then two locations are found: the mean
of the input positions, and the pole to the great circle which best fits
the input positions. The user may choose one or both of two possible
solutions to this problem. When the data are closely grouped along a
great circle both solutions are similar. If the data have large
dispersion, the pole to the great circle will be less well determined
than the mean. Compare both solutions as a qualitative check.

Setting ``norm`` to **1** approximates the minimization of the sum of
absolute values of cosines of angular distances. This solution finds the
mean position as the Fisher average of the data, and the pole position
as the Fisher average of the cross-products between the mean and the
data. Averaging cross-products gives weight to points in proportion to
their distance from the mean, analogous to the "leverage" of distant
points in linear regression in the plane.

Setting ``norm`` to **2** approximates the minimization of the sum of
squares of cosines of angular distances. It creates a 3 by 3 matrix of
sums of squares of components of the data vectors. The eigenvectors of
this matrix give the mean and pole locations. This method may be more
subject to roundoff errors when there are thousands of data. The pole is
given by the eigenvector corresponding to the smallest eigenvalue; it is
the least-well represented factor in the data and is not easily
estimated by either method.

Full GMT docs at :gmt-docs:`fitcircle.html`.

$aliases
- V = verbose
Comment thread
willschlitzer marked this conversation as resolved.
Outdated

Parameters
----------
data
Pass in (longitude, latitude) or (latitude, longitude) values by
Comment thread
willschlitzer marked this conversation as resolved.
Outdated
providing a file name to an ASCII data table, a 2-D
$table_classes.
$output_type
$outfile
norm : int or bool

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1, 2, 3 are not readable arguments. GMT recommends absolutes/squares/both instead.

https://github.com/GenericMappingTools/gmt/blob/d52a1e5825b73c3ad6948efce1ac8eeff01c2c93/src/longopt/fitcircle_inc.h#L32-33

As for the parameter, GMT supports norm and solution, while GMT.jl uses norm (https://www.generic-mapping-tools.org/GMTjl_doc/documentation/modules/fitcircle.html).

Specify the desired *norm* as **1** or **2**\ , or use ``True`` or
**3** to see both solutions. Note that ``output_type="pandas"`` is
not supported when ``norm`` is ``True`` or **3**; use
``output_type="numpy"`` or ``output_type="file"`` instead.
small_circle : bool or float
Comment thread
willschlitzer marked this conversation as resolved.
Outdated
Attempt to fit a small circle instead of a great circle. The pole
will be constrained to lie on the great circle connecting the pole
of the best-fit great circle and the mean location of the data.
Optionally append the desired fixed latitude of the small circle
[Default will determine the optimal latitude].
Comment thread
willschlitzer marked this conversation as resolved.
Outdated
$verbose

Returns
-------
ret
Return type depends on ``outfile`` and ``output_type``:

- ``None`` if ``outfile`` is set (output will be stored in the file set by
``outfile``)
- :class:`pandas.DataFrame` or :class:`numpy.ndarray` if ``outfile`` is not set
(depends on ``output_type``)
"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if kwargs.get("L") is None:
raise GMTParameterError(required="norm")

output_type = validate_output_table_type(output_type, outfile=outfile)
norm = kwargs.get("L")
if output_type == "pandas" and (norm is True or norm == 3):
raise GMTValueError(
norm,
description="value for parameter 'norm'",
reason=(
"Pandas output is not supported when 'norm' is set to True or 3 "
"since both L1 and L2 solutions are stacked in the same rows. "
"Use output_type='numpy' or output_type='file' instead."
),
)

aliasdict = AliasSystem().add_common(
V=verbose,
)
aliasdict.merge(kwargs)

with Session() as lib:
with (
lib.virtualfile_in(check_kind="vector", data=data) as vintbl,
lib.virtualfile_out(kind="dataset", fname=outfile) as vouttbl,
):
lib.call_module(
module="fitcircle",
args=build_arg_list(aliasdict, infile=vintbl, outfile=vouttbl),
)
return lib.virtualfile_to_dataset(
vfname=vouttbl,
output_type=output_type,
column_names=["longitude", "latitude", "method"],

@weiji14 weiji14 Aug 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These output columns might change depending on whether -L/norm=1/2 or -L/norm=3/True ? Could we do something like:

column_names=["longitude", "latitude", "method"] if kwargs.get("L") in {1, 2} else ["longitude_l1", "latitude_l1", "longitude_l2", "latitude_l2"],

or whatever column names make sense for norm=3.

Alternatively, we can just disable the ability to output to pandas format for norm=3/norm=True if it gets too complicated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to disable the pandas format for norm=3/True, as that seemed more straightforward.

)
130 changes: 130 additions & 0 deletions pygmt/tests/test_fitcircle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""
Test pygmt.fitcircle.
"""

from pathlib import Path

import numpy as np
import numpy.testing as npt
import pandas as pd
import pytest
from pygmt import fitcircle
from pygmt.exceptions import GMTParameterError, GMTValueError
from pygmt.helpers import GMTTempFile
from pygmt.src import which


@pytest.fixture(scope="module", name="data")
def fixture_data():
"""
Load the sample data from the @sat_03 remote file.
"""
fname = which("@sat_03.txt", download="c")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This @sat_03.txt file will need to be added to the cache list at https://github.com/GenericMappingTools/pygmt/blob/v0.19.0/pygmt/helpers/caching.py

return pd.read_csv(
fname, header=None, skiprows=1, sep="\t", names=["longitude", "latitude", "z"]
)


@pytest.mark.benchmark
def test_fitcircle_no_outfile(data):
"""
Test fitcircle with no set outfile.
"""
result = fitcircle(data=data, norm=2)
assert isinstance(result, pd.DataFrame)
assert result.shape == (4, 3)
# Test longitude results
npt.assert_allclose(result.longitude.min(), 52.7449849947)
npt.assert_allclose(result.longitude.max(), 330.243649573)
# Test latitude results
npt.assert_allclose(result.latitude.min(), -21.2046833116)
npt.assert_allclose(result.latitude.max(), 21.2046833116)


def test_fitcircle_file_output(data):
"""
Test that fitcircle returns a file output when it is specified.
"""
with GMTTempFile(suffix=".txt") as tmpfile:
result = fitcircle(
data=data, norm=True, outfile=tmpfile.name, output_type="file"
)
assert result is None # return value is None
assert Path(tmpfile.name).stat().st_size > 0 # check that outfile exists


def test_fitcircle_invalid_format(data):
"""
Test that fitcircle fails with an incorrect format for output_type.
"""
with pytest.raises(GMTValueError):
fitcircle(data=data, norm=True, output_type="a")


def test_fitcircle_no_norm(data):
"""
Test that fitcircle fails when the required "norm" parameter is missing.
"""
with pytest.raises(GMTParameterError):
fitcircle(data=data)


def test_fitcircle_no_outfile_specified(data):
"""
Test that fitcircle fails when output_type is set to "file" but no outfile
is specified.
"""
with pytest.raises(GMTParameterError):
fitcircle(data=data, norm=True, output_type="file")


def test_fitcircle_outfile_incorrect_output_type(data):
"""
Test that fitcircle raises a warning when an outfile filename is set but the
output_type is not set to "file".
"""
with GMTTempFile(suffix=".txt") as tmpfile:
with pytest.warns(RuntimeWarning) as record:
result = fitcircle(
data=data, norm=True, outfile=tmpfile.name, output_type="numpy"
)
assert len(record) == 1 # check that only one warning was raised
assert result is None # return value is None
assert Path(tmpfile.name).stat().st_size > 0 # check that outfile exists


def test_fitcircle_format(data):
"""
Test that correct formats are returned.
"""
circle_default = fitcircle(data=data, norm=2)
assert isinstance(circle_default, pd.DataFrame)
circle_array = fitcircle(data=data, norm=2, output_type="numpy")
assert isinstance(circle_array, np.ndarray)
circle_df = fitcircle(data=data, norm=2, output_type="pandas")
assert isinstance(circle_df, pd.DataFrame)


@pytest.mark.parametrize("norm", [True, 3])
def test_fitcircle_pandas_unsupported_for_both_norms(data, norm):
"""
Test that fitcircle raises an exception when output_type is "pandas" (the
default) and norm is True or 3, since the L1 and L2 solutions are stacked
in the same rows and can't be represented as a single pandas.DataFrame.
"""
with pytest.raises(GMTValueError):
fitcircle(data=data, norm=norm)
with pytest.raises(GMTValueError):
fitcircle(data=data, norm=norm, output_type="pandas")
result = fitcircle(data=data, norm=norm, output_type="numpy")
assert isinstance(result, np.ndarray)


def test_fitcircle_small_circle(data):
"""
Test that fitcircle can fit a small circle instead of a great circle.
"""
result = fitcircle(data=data, norm=2, small_circle=True)
assert isinstance(result, pd.DataFrame)
assert result.shape == (5, 3)
assert "Small Circle Pole" in result.method.iloc[-1]
Loading