Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PERF: Optimize for single point in Geod fwd/inv functions #1206

Merged
merged 1 commit into from
Dec 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 18 additions & 0 deletions pyproj/_geod.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ class Geod:
radians: bool = False,
return_back_azimuth: bool = True,
) -> None: ...
def _fwd_point(
self,
lons: float,
lats: float,
az: float,
dist: float,
radians: bool = False,
return_back_azimuth: bool = True,
) -> Tuple[float, float, float]: ...
def _inv(
self,
lons1: Any,
Expand All @@ -40,6 +49,15 @@ class Geod:
radians: bool = False,
return_back_azimuth: bool = False,
) -> None: ...
def _inv_point(
self,
lons1: float,
lats1: float,
lons2: float,
lats2: float,
radians: bool = False,
return_back_azimuth: bool = False,
) -> Tuple[float, float, float]: ...
def _inv_or_fwd_intermediate(
self,
lon1: float,
Expand Down
107 changes: 107 additions & 0 deletions pyproj/_geod.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,63 @@ cdef class Geod:
latbuff.data[iii] = _DG2RAD * plat2
azbuff.data[iii] = _DG2RAD * pazi2

@cython.boundscheck(False)
@cython.wraparound(False)
def _fwd_point(
self,
object lon1in,
object lat1in,
object az1in,
object s12in,
bint radians=False,
bint return_back_azimuth=True,
):
"""
Scalar optimized function
forward transformation - determine longitude, latitude and back azimuth
of a terminus point given an initial point longitude and latitude, plus
forward azimuth and distance.
if radians=True, lons/lats are radians instead of degrees.
"""
cdef double plon2, plat2, pazi2

# We do the type-checking internally here rather than declaring double as the
# type into the function due to automatically casting length-1 arrays to float
# that we don't want to return scalar for.
# Ex: float(np.array([0])) works and we don't want to accept numpy arrays
cdef double lon1 = lon1in
cdef double lat1 = lat1in
cdef double az1 = az1in
cdef double s12 = s12in
for x in (lon1in, lat1in, az1in, s12in):
if not isinstance(x, (float, int)):
raise TypeError("Scalar input is required for point based functions")

with nogil:
if radians:
lon1 = _RAD2DG * lon1
lat1 = _RAD2DG * lat1
az1 = _RAD2DG * az1
geod_direct(
&self._geod_geodesic,
lat1,
lon1,
az1,
s12,
&plat2,
&plon2,
&pazi2,
)
# back azimuth needs to be flipped 180 degrees
# to match what PROJ geod utility produces.
if return_back_azimuth:
pazi2 =_reverse_azimuth(pazi2, factor=180)
if radians:
plon2 = _DG2RAD * plon2
plat2 = _DG2RAD * plat2
pazi2 = _DG2RAD * pazi2
return plon2, plat2, pazi2

@cython.boundscheck(False)
@cython.wraparound(False)
def _inv(
Expand Down Expand Up @@ -227,6 +284,56 @@ cdef class Geod:
# write azimuth data into lon2 buffer
lon2buff.data[iii] = ps12

@cython.boundscheck(False)
@cython.wraparound(False)
def _inv_point(
self,
object lon1in,
object lat1in,
object lon2in,
object lat2in,
bint radians=False,
bint return_back_azimuth=True,
):
"""
Scalar optimized function
inverse transformation - return forward and back azimuth, plus distance
between an initial and terminus lat/lon pair.
if radians=True, lons/lats are radians instead of degree
"""
cdef double pazi1, pazi2, ps12
# We do the type-checking internally here rather than declaring double as the
# type into the function due to automatically casting length-1 arrays to float
# that we don't want to return scalar for.
# Ex: float(np.array([0])) works and we don't want to accept numpy arrays
cdef double lon1 = lon1in
cdef double lat1 = lat1in
cdef double lon2 = lon2in
cdef double lat2 = lat2in
for x in (lon1in, lat1in, lon2in, lat2in):
if not isinstance(x, (float, int)):
raise TypeError("Scalar input is required for point based functions")

with nogil:
if radians:
lon1 = _RAD2DG * lon1
lat1 = _RAD2DG * lat1
lon2 = _RAD2DG * lon2
lat2 = _RAD2DG * lat2
geod_inverse(
&self._geod_geodesic,
lat1, lon1, lat2, lon2,
&ps12, &pazi1, &pazi2,
)
# back azimuth needs to be flipped 180 degrees
# to match what proj4 geod utility produces.
if return_back_azimuth:
pazi2 =_reverse_azimuth(pazi2, factor=180)
if radians:
pazi1 = _DG2RAD * pazi1
pazi2 = _DG2RAD * pazi2
return pazi1, pazi2, ps12

@cython.boundscheck(False)
@cython.wraparound(False)
def _inv_or_fwd_intermediate(
Expand Down
34 changes: 31 additions & 3 deletions pyproj/geod.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ def fwd( # pylint: disable=invalid-name
instead of returning a new array. This will fail if the input
is not an array in C order with the double data type.
return_back_azimuth: bool, default=True
if True, the third return value will be the back azimuth,
If True, the third return value will be the back azimuth,
Otherwise, it will be the forward azimuth.

Returns
Expand All @@ -306,6 +306,20 @@ def fwd( # pylint: disable=invalid-name
scalar or array:
Back azimuth(s) or Forward azimuth(s)
"""
try:
# Fast-path for scalar input, will raise if invalid types are input
# and we can fallback below
return self._fwd_point(
lons,
lats,
az,
dist,
radians=radians,
return_back_azimuth=return_back_azimuth,
)
except TypeError:
pass

# process inputs, making copies that support buffer API.
inx, x_data_type = _copytobuffer(lons, inplace=inplace)
iny, y_data_type = _copytobuffer(lats, inplace=inplace)
Expand Down Expand Up @@ -371,8 +385,8 @@ def inv(
instead of returning a new array. This will fail if the input
is not an array in C order with the double data type.
return_back_azimuth: bool, default=True
if True, the second return value (azi21) will be the back azimuth
(flipped 180 degrees), Otherwise, it will be also a forward azimuth.
If True, the second return value (azi21) will be the back azimuth
(flipped 180 degrees), Otherwise, it will also be a forward azimuth.

Returns
-------
Expand All @@ -384,6 +398,20 @@ def inv(
Distance(s) between initial and terminus point(s)
in meters
"""
try:
# Fast-path for scalar input, will raise if invalid types are input
# and we can fallback below
return self._inv_point(
lons1,
lats1,
lons2,
lats2,
radians=radians,
return_back_azimuth=return_back_azimuth,
)
except TypeError:
pass

# process inputs, making copies that support buffer API.
inx, x_data_type = _copytobuffer(lons1, inplace=inplace)
iny, y_data_type = _copytobuffer(lats1, inplace=inplace)
Expand Down
45 changes: 45 additions & 0 deletions test/test_geod.py
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,51 @@ def test_geod_fwd_honours_input_types(lon, lat, az):
assert isinstance(outz, type(az))


def test_geod_fwd_radians():
g = Geod(ellps="clrk66")
lon1 = 1
lat1 = 1
az1 = 1
dist = 1
assert_almost_equal(
np.rad2deg(g.fwd(lon1, lat1, az1, dist, radians=True)),
g.fwd(lon1 * 180 / np.pi, lat1 * 180 / np.pi, az1 * 180 / np.pi, dist),
)


def test_geod_inv_radians():
g = Geod(ellps="clrk66")
lon1 = 0
lat1 = 0
lon2 = 1
lat2 = 1
# the third output is in distance, so we don't want to change from deg-rad there
out_rad = list(g.inv(lon1, lat1, lon2, lat2, radians=True))
out_rad[0] *= 180 / np.pi
out_rad[1] *= 180 / np.pi
assert_almost_equal(
out_rad,
g.inv(
lon1 * 180 / np.pi,
lat1 * 180 / np.pi,
lon2 * 180 / np.pi,
lat2 * 180 / np.pi,
),
)


@pytest.mark.parametrize("func_name", ("fwd", "inv"))
@pytest.mark.parametrize("radians", (True, False))
def test_geod_scalar_array(func_name, radians):
# verify two singlepoint calculations match an array of length two
g = Geod(ellps="clrk66")
func = getattr(g, func_name)
assert_almost_equal(
np.transpose([func(0, 0, 1, 1, radians=radians) for i in range(2)]),
func([0, 0], [0, 0], [1, 1], [1, 1], radians=radians),
)


@pytest.mark.parametrize(
"lons1,lats1,lons2", permutations([10.0, [10.0], (10.0,)])
) # 6 test cases
Expand Down