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

Python 3.11 compatible versions of numba JIT-compiled functions #20

Merged
merged 11 commits into from
Jun 29, 2023
2 changes: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10"]
python-version: ["3.9", "3.10", "3.11"]

steps:
- uses: actions/checkout@main
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## v0.3.0 (2023-06-29)
- Python 3.11 compatible versions of numba JIT-compiled functions
- drop Python 3.8 support

## v0.2.14 (2023-04-17)
- avgbinmap / mean_day_frac: bugfix corner case mean day fraction is almost zero but < 0
- avgbinmap / add scipy based version of mean angle function
Expand Down
777 changes: 378 additions & 399 deletions poetry.lock

Large diffs are not rendered by default.

31 changes: 19 additions & 12 deletions pyfuppes/avgbinmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from cmath import phase, rect
from copy import deepcopy
from math import degrees, radians
from math import cos, sin, atan2, pi

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -47,23 +48,29 @@ def mean_angle(deg):


@njit
def mean_angle_numba(deg):
def mean_angle_numba(angles):
"""
Numba-compatible version of mean_angle().
mean_angle(), numba-JIT compiled.

- input must be numpy array of type float!

C version: https://rosettacode.org/wiki/Averages/Mean_angle
"""
deg = deg[np.isfinite(deg)]
if len(deg) == 0:
angles = angles[np.isfinite(angles)]
if len(angles) == 0:
return np.nan
elif len(deg) == 1:
return deg[0]
elif len(angles) == 1:
return angles[0]

size = len(angles)
y_part = 0.0
x_part = 0.0

result = 0
for d in deg:
result += rect(1, radians(d))
for i in range(size):
x_part += cos(angles[i] * pi / 180)
y_part += sin(angles[i] * pi / 180)

return degrees(phase(result / len(deg)))
return atan2(y_part / size, x_part / size) * 180 / pi


###############################################################################
Expand Down Expand Up @@ -194,7 +201,7 @@ def bin_t_10s(t, force_t_range=True, drop_empty=True):

@njit
def get_npnanmean(v):
"""Njit'ed nan-mean."""
"""nan-mean, numba-JIT compiled."""
return np.nanmean(v)


Expand Down Expand Up @@ -579,7 +586,7 @@ def calc_shift(
_tol: float = 1e-9,
) -> np.ndarray:
"""
Calculate shift values that, when added to arr, put the values of arr on a regular grid.
Calculate shift-values that, when added to arr, put the values of arr on a regular grid. Code gets numba-JIT compiled.

Parameters
----------
Expand Down
36 changes: 17 additions & 19 deletions pyfuppes/geo.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
"""Geospatial helpers, such as Haversine distance or solar zenith angle."""

import math
from datetime import datetime
from math import cos, sin, acos, asin, radians, sqrt, degrees, floor

from geopy import distance
from numba import njit
Expand All @@ -13,25 +13,23 @@

@njit
def haversine_dist(lat, lon):
"""Calculate Haversine distance along lat/lon coordinates."""
"""Calculate Haversine distance along lat/lon coordinates in km. Code gets numba-JIT compiled."""
assert lat.shape[0] == lon.shape[0], "lat/lon must be of same length."
R = 6373 # approximate radius of earth in km
R = 6372.8 # approximate radius of earth in km
dist = 0

lat1 = math.radians(lat[0])
lon1 = math.radians(lon[0])
lat1, lon1 = lat[0], lon[0]
for j in range(1, lat.shape[0]):
lat0, lat1 = lat1, math.radians(lat[j])
lon0, lon1 = lon1, math.radians(lon[j])
lat0, lat1 = lat1, lat[j]
lon0, lon1 = lon1, lon[j]

dlon = lon1 - lon0
dlat = lat1 - lat0
dLat = radians(lat1 - lat0)
dLon = radians(lon1 - lon0)
lat0 = radians(lat0)
lat1 = radians(lat1)

a = (
math.sin(dlat / 2) ** 2
+ math.cos(lat0) * math.cos(lat1) * math.sin(dlon / 2) ** 2
)
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
a = sin(dLat / 2) ** 2 + cos(lat0) * cos(lat1) * sin(dLon / 2) ** 2
c = 2 * asin(sqrt(a))

dist += R * c

Expand Down Expand Up @@ -79,13 +77,13 @@ def sza(UTC=datetime.utcnow(), latitude=52.37, longitude=9.72):

# define trigonometry with degrees
def cos2(x):
return math.cos(math.radians(x))
return cos(radians(x))

def sin2(x):
return math.sin(math.radians(x))
return sin(radians(x))

def acos2(x):
return math.degrees(math.acos(x))
return degrees(acos(x))

# parameter
day_of_year = UTC.timetuple().tm_yday
Expand Down Expand Up @@ -132,7 +130,7 @@ def get_EoT(date_ts):
use for: calculation of local solar time
"""
B = (360 / 365) * (date_ts.timetuple().tm_yday - 81)
return 9.87 * math.sin(2 * B) - 7.53 * math.cos(B) - 1.5 * math.sin(B)
return 9.87 * sin(2 * B) - 7.53 * cos(B) - 1.5 * sin(B)


###############################################################################
Expand All @@ -155,5 +153,5 @@ def get_LSTdayFrac(longitude, tz_offset, EoT, days_delta, time_delta):
t_corr = (4 * (longitude - LSTM) + EoT) / 60 / 24 # [d]
LST_frac = (time_delta + tz_offset / 24 - days_delta) + t_corr
if LST_frac > 1:
LST_frac -= math.floor(LST_frac)
LST_frac -= floor(LST_frac)
return LST_frac
9 changes: 4 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[tool.poetry]
name = "pyfuppes"
version = "0.2.14"
version = "0.3.0"
description = "A collection of tools in Python"
authors = ["Florian Obersteiner <f.obersteiner@kit.edu>"]
license = "MIT"
readme = "README.md"

[tool.poetry.dependencies]
python = ">= 3.8.1, < 3.11" # indirect; numba ...
python = ">= 3.9, < 3.12"
geopy = ">= 2.0"
matplotlib = ">= 3.0"
pandas = ">= 1.0"
Expand All @@ -18,9 +18,8 @@ scipy = ">= 1.1"
netcdf4 = ">= 1.6"
xarray = ">= 2022, >=2023"
tomli = ">= 2.0.1"
numba = "^0.56.4" # no python 3.11 support as of 2023-02-10
numpy = "<1.24, >=1.18" # required by numba 0.56.4
llvmlite = "<0.40, >=0.39.0dev0" # required by numba 0.56.4
numba = ">= 0.56.4"
numpy = ">= 1.18"

[tool.poetry.dev-dependencies]
pytest = ">=7.0"
Expand Down
2 changes: 1 addition & 1 deletion tests/test_avgbin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pyfuppes import avgbinmap


class TestTimeconv(unittest.TestCase):
class TestAvgbinmap(unittest.TestCase):
@classmethod
def setUpClass(cls):
# to run before all tests
Expand Down
2 changes: 1 addition & 1 deletion tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pyfuppes import filters


class TestTimeconv(unittest.TestCase):
class TestFilters(unittest.TestCase):
@classmethod
def setUpClass(cls):
# to run before all tests
Expand Down
40 changes: 40 additions & 0 deletions tests/test_geo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-

import unittest

import numpy as np

from pyfuppes import geo


class TestGeo(unittest.TestCase):
@classmethod
def setUpClass(cls):
# to run before all tests
print("\ntesting pyfuppes.avgbin...")

@classmethod
def tearDownClass(cls):
# to run after all tests
pass

def setUp(self):
# to run before each test
pass

def tearDown(self):
# to run after each test
pass

def test_haversine_dist(self):
dist = 2887
tol_decimalplaces = 0
self.assertAlmostEqual(
geo.haversine_dist(np.array((36.12, 33.94)), np.array((-86.67, -118.40))),
dist,
tol_decimalplaces,
)


if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion tests/test_interpolate.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pyfuppes import interpolate


class TestCfg(unittest.TestCase):
class TestInterpolate(unittest.TestCase):
@classmethod
def setUpClass(cls):
pass
Expand Down
2 changes: 1 addition & 1 deletion tests/test_timecorr.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def _make_df(dt_list):
)


class TestTimeconv(unittest.TestCase):
class TestTimecorr(unittest.TestCase):
@classmethod
def setUpClass(cls):
# to run before all tests
Expand Down
2 changes: 1 addition & 1 deletion tests/test_v25.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
src, dst = wd / "test_input", wd / "test_output"


class TestTimeconv(unittest.TestCase):
class TestV25tools(unittest.TestCase):
@classmethod
def setUpClass(cls):
# to run before all tests
Expand Down