Skip to content

Commit

Permalink
PERF: Optimize point transformations (#1204)
Browse files Browse the repository at this point in the history
  • Loading branch information
snowman2 committed Dec 21, 2022
1 parent 59d16f5 commit 39ae460
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 51 deletions.
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ disable=cyclic-import,


[FORMAT]
max-module-lines=1350
max-module-lines=1500
4 changes: 1 addition & 3 deletions docs/history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ Change Log

Latest
------

3.5.0
-----
- ENH: Add `return_back_azimuth: bool` to allow compatibility between the azimuth output of the following functions (issue #1163):
`fwd` and `fwd_intermediate`, `inv` and `inv_intermediate`,
Note: BREAKING CHANGE for the default value `return_back_azimuth=True` in the functions `fwd_intermediate` and `inv_intermediate`
to mach the default value in `fwd` and `inv`
- PERF: Optimize point transformations (pull #1204)

3.4.1
-----
Expand Down
11 changes: 11 additions & 0 deletions pyproj/_transformer.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import numbers
from array import array
from typing import Any, List, NamedTuple, Optional, Tuple, Union

Expand Down Expand Up @@ -96,6 +97,16 @@ class _Transformer(Base):
radians: bool,
errcheck: bool,
) -> None: ...
def _transform_point(
self,
inx: numbers.Real,
iny: numbers.Real,
inz: numbers.Real,
intime: numbers.Real,
direction: Union[TransformDirection, str],
radians: bool,
errcheck: bool,
) -> None: ...
def _transform_sequence(
self,
stride: int,
Expand Down
85 changes: 85 additions & 0 deletions pyproj/_transformer.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,91 @@ cdef class _Transformer(Base):
ybuff.data[iii] = ybuff.data[iii]*_DG2RAD
_clear_proj_error()

@cython.boundscheck(False)
@cython.wraparound(False)
def _transform_point(
self,
object inx,
object iny,
object inz,
object intime,
object direction,
bint radians,
bint errcheck,
):
"""
Optimized to transform a single point between two coordinate systems.
"""
cdef double coord_x = inx
cdef double coord_y = iny
cdef double coord_z = 0
cdef double coord_t = HUGE_VAL
cdef tuple expected_numeric_types = (int, float)
if not isinstance(inx, expected_numeric_types):
raise TypeError("Scalar input expected for x")
if not isinstance(iny, expected_numeric_types):
raise TypeError("Scalar input expected for y")
if inz is not None:
if not isinstance(inz, expected_numeric_types):
raise TypeError("Scalar input expected for z")
coord_z = inz
if intime is not None:
if not isinstance(intime, expected_numeric_types):
raise TypeError("Scalar input expected for t")
coord_t = intime

cdef tuple return_data
if self.id == "noop":
return_data = (inx, iny)
if inz is not None:
return_data += (inz,)
if intime is not None:
return_data += (intime,)
return return_data

cdef PJ_DIRECTION pj_direction = get_pj_direction(direction)
cdef PJ_COORD projxyout
cdef PJ_COORD projxyin = proj_coord(coord_x, coord_y, coord_z, coord_t)
with nogil:
# degrees to radians
if not radians and proj_angular_input(self.projobj, pj_direction):
projxyin.uv.u *= _DG2RAD
projxyin.uv.v *= _DG2RAD
# radians to degrees
elif radians and proj_degree_input(self.projobj, pj_direction):
projxyin.uv.u *= _RAD2DG
projxyin.uv.v *= _RAD2DG

proj_errno_reset(self.projobj)
projxyout = proj_trans(self.projobj, pj_direction, projxyin)
errno = proj_errno(self.projobj)
if errcheck and errno:
with gil:
raise ProjError(
f"transform error: {proj_context_errno_string(self.context, errno)}"
)
elif errcheck:
with gil:
if _clear_proj_error() is not None:
raise ProjError("transform error")

# radians to degrees
if not radians and proj_angular_output(self.projobj, pj_direction):
projxyout.xy.x *= _RAD2DG
projxyout.xy.y *= _RAD2DG
# degrees to radians
elif radians and proj_degree_output(self.projobj, pj_direction):
projxyout.xy.x *= _DG2RAD
projxyout.xy.y *= _DG2RAD
_clear_proj_error()

return_data = (projxyout.xyzt.x, projxyout.xyzt.y)
if inz is not None:
return_data += (projxyout.xyzt.z,)
if intime is not None:
return_data += (projxyout.xyzt.t,)
return return_data

@cython.boundscheck(False)
@cython.wraparound(False)
def _transform_sequence(
Expand Down
1 change: 1 addition & 0 deletions pyproj/proj.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ cdef extern from "proj.h" nogil:
int proj_degree_input (PJ *P, PJ_DIRECTION dir)
int proj_degree_output (PJ *P, PJ_DIRECTION dir)

PJ_COORD proj_trans (PJ *P, PJ_DIRECTION direction, PJ_COORD coord)
size_t proj_trans_generic (
PJ *P,
PJ_DIRECTION direction,
Expand Down
17 changes: 15 additions & 2 deletions pyproj/transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,19 @@ def transform( # pylint: disable=invalid-name
'33 98'
"""
try:
# function optimized for point data
return self._transformer._transform_point(
inx=xx,
iny=yy,
inz=zz,
intime=tt,
direction=direction,
radians=radians,
errcheck=errcheck,
)
except TypeError:
pass
# process inputs, making copies that support buffer API.
inx, x_data_type = _copytobuffer(xx, inplace=inplace)
iny, y_data_type = _copytobuffer(yy, inplace=inplace)
Expand All @@ -802,8 +815,8 @@ def transform( # pylint: disable=invalid-name
intime = None
# call pj_transform. inx,iny,inz buffers modified in place.
self._transformer._transform(
inx,
iny,
inx=inx,
iny=iny,
inz=inz,
intime=intime,
direction=direction,
Expand Down
26 changes: 26 additions & 0 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from contextlib import contextmanager
from pathlib import Path

import numpy
import pytest
from packaging import version

import pyproj
Expand Down Expand Up @@ -90,3 +92,27 @@ def assert_can_pickle(raw_obj, tmp_path):
unpickled = pickle.load(f)

assert raw_obj == unpickled


def _make_longer_array(data: float):
"""
Turn the float into a 2-element array
"""
return numpy.array([data] * 2)


@pytest.fixture(
params=[
float,
numpy.array,
_make_longer_array,
]
)
def scalar_and_array(request):
"""
Ensure cython methods are tested
with scalar and arrays to trigger
point optimized functions as well
as the main functions supporting arrays.
"""
return request.param

0 comments on commit 39ae460

Please sign in to comment.