Skip to content

Commit 0ba8ba7

Browse files
authored
0.0.4
### 0.0.4 * **Added Weyl and Riesz Fractional Derivative Functions:** * **`Weyl`**: Implements the periodic, right-sided fractional derivative using an FFT-based method. For periodic functions, this operator produces phase-shifted analytical derivatives, and is especially well-suited for spectral methods. * **`Riesz`**: Implements the symmetric (two-sided) fractional derivative, also using FFT. For pure sines/cosines, the Riesz operator multiplies each Fourier mode by $-|k|^\alpha$, resulting in a real-valued, sign-flipped output (not a phase shift). Especially relevant in physics and PDEs. * **Testing Update:** * Both `Weyl` and `Riesz` are covered by **unittest** and **pytest**-style tests for compatibility and reliability. * The test suite is transitioned from `unittest` to `pytest` for modern, flexible, and more expressive testing.
2 parents 3621c75 + c3ee27a commit 0ba8ba7

15 files changed

Lines changed: 567 additions & 493 deletions

.github/workflows/python-package.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ jobs:
3636
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
3737
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
3838
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
39-
- name: Test with unittest
40-
run: |
41-
python -m unittest discover
39+
# - name: Test with unittest
40+
# run: |
41+
# python -m unittest discover
42+
43+
# === Added for pytest ===
44+
- name: Test with pytest # [pytest]
45+
run: | # [pytest]
46+
pytest # [pytest]

changelog.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,16 @@
156156
- `RL` `RLpoint` `PCsolver` functions Namespaces
157157

158158
- Aditional test and test restructuring
159-
- Restructuring the Namespaces
159+
- Restructuring the Namespaces
160+
161+
162+
### 0.0.4
163+
164+
* **Added Weyl and Riesz Fractional Derivative Functions:**
165+
166+
* **`Weyl`**: Implements the periodic, right-sided fractional derivative using an FFT-based method. For periodic functions, this operator produces phase-shifted analytical derivatives, and is especially well-suited for spectral methods.
167+
* **`Riesz`**: Implements the symmetric (two-sided) fractional derivative, also using FFT. For pure sines/cosines, the Riesz operator multiplies each Fourier mode by $-|k|^\alpha$, resulting in a real-valued, sign-flipped output (not a phase shift). Especially relevant in physics and PDEs.
168+
* **Testing Update:**
169+
170+
* Both `Weyl` and `Riesz` are covered by **unittest** and **pytest**-style tests for compatibility and reliability.
171+
* The test suite is transitioned from `unittest` to `pytest` for modern, flexible, and more expressive testing.

differintP/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
CaputoFromRLpoint,
1616
)
1717

18+
from .weyl import Weyl, Riesz
19+
1820
from . import functions
1921

2022
__all__ = [
23+
# Core
2124
"GLcoeffs",
2225
"GL",
2326
"GLpoint",
@@ -33,5 +36,9 @@
3336
"CaputoL2Cpoint",
3437
"CaputoFromRLpoint",
3538

39+
# weyl
40+
"Weyl",
41+
"Riesz",
42+
3643
"functions"
3744
]

differintP/weyl.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from typing import Callable, cast, Union, List
2+
3+
import numpy as np
4+
5+
def Weyl(
6+
alpha: float,
7+
f_name: Union[np.ndarray, List[float], Callable],
8+
domain_start: float = 0.0,
9+
domain_end: float = 2 * np.pi,
10+
num_points: int = 100,
11+
) -> np.ndarray:
12+
"""
13+
Weyl fractional derivative (periodic, Fourier-based).
14+
15+
Numerically computes the Weyl (right-sided, periodic) fractional derivative of order `alpha`
16+
for a function on a uniform grid, using the FFT. This method is fast and accurate for
17+
periodic functions on [domain_start, domain_end].
18+
19+
References:
20+
- Samko, Kilbas, Marichev, *Fractional Integrals and Derivatives* (see Weyl derivative, Ch. 7)
21+
22+
Parameters
23+
----------
24+
alpha : float
25+
Order of the derivative.
26+
f_name : callable or array-like
27+
Function or array of values to differentiate.
28+
domain_start, domain_end : float
29+
Interval (should cover one period for periodic functions).
30+
num_points : int
31+
Number of grid points.
32+
33+
Returns
34+
-------
35+
df : np.ndarray
36+
Array of Weyl fractional derivative values at grid points.
37+
"""
38+
39+
if callable(f_name):
40+
# f_name = cast(Callable, f_name) # type checking
41+
# If f_name is callable, call it and save to a list.
42+
x = np.linspace(domain_start, domain_end, num_points, endpoint=False)
43+
f_values = f_name(x)
44+
else:
45+
f_name = cast(np.ndarray | list[float], f_name)
46+
num_points = np.size(f_name)
47+
f_values = f_name
48+
49+
# Compute FFT
50+
fhat = np.fft.fft(f_values) # type: ignore
51+
L = domain_end - domain_start
52+
k = np.fft.fftfreq(num_points, d=L / num_points) * 2 * np.pi # Frequency in radians
53+
54+
# Fractional derivative in Fourier domain
55+
multiplier = np.zeros_like(k, dtype=complex)
56+
multiplier[1:] = (1j * k[1:]) ** alpha # k=0 stays zero
57+
58+
fhat_new = fhat * multiplier
59+
df = np.fft.ifft(fhat_new)
60+
return df.real if np.all(np.isreal(f_values)) else df # type: ignore
61+
62+
63+
def Riesz(
64+
alpha: float,
65+
f_name: Union[np.ndarray, List[float], Callable],
66+
domain_start: float = 0.0,
67+
domain_end: float = 2 * np.pi,
68+
num_points: int = 100,
69+
) -> np.ndarray:
70+
"""
71+
Riesz fractional derivative (symmetric, Fourier-based).
72+
73+
Numerically computes the Riesz fractional derivative of order `alpha`
74+
for a function on a uniform grid using the FFT. This operator is
75+
symmetric (unlike Weyl) and real for real input.
76+
77+
Parameters
78+
----------
79+
alpha : float
80+
Order of the derivative.
81+
f_name : callable or array-like
82+
Function or array of values to differentiate.
83+
domain_start, domain_end : float
84+
Interval (should cover one period for periodic functions).
85+
num_points : int
86+
Number of grid points.
87+
88+
Returns
89+
-------
90+
df : np.ndarray
91+
Array of Riesz fractional derivative values at grid points.
92+
"""
93+
if callable(f_name):
94+
x = np.linspace(domain_start, domain_end, num_points, endpoint=False)
95+
f_values = f_name(x)
96+
else:
97+
f_values = np.asarray(f_name)
98+
num_points = len(f_values)
99+
100+
# FFT
101+
fhat = np.fft.fft(f_values)
102+
L = domain_end - domain_start
103+
k = np.fft.fftfreq(num_points, d=L / num_points) * 2 * np.pi # radians
104+
105+
# Riesz symbol: -|k|^alpha (symmetric)
106+
multiplier = -np.abs(k) ** alpha
107+
108+
# Apply in Fourier domain
109+
fhat_new = fhat * multiplier
110+
df = np.fft.ifft(fhat_new)
111+
return df.real if np.all(np.isreal(f_values)) else df
112+
113+

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "differintP"
7-
version = "0.0.3"
7+
version = "0.0.4"
88
description = "Modern, pure Python fractional calculus library (fork of differint)"
99
readme = "README.md"
1010
requires-python = ">=3.10"

test_pytest/test_core.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import numpy as np
2+
3+
import sys
4+
import os
5+
6+
# Add the parent directory (containing differintP) to the import path
7+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
8+
9+
from differintP.core import * # type: ignore
10+
from differintP.functions import MittagLeffler
11+
from .__init__ import test_N
12+
13+
# Define constants to be used in tests.
14+
size_coefficient_array = 20
15+
sqrtpi2 = 0.88622692545275794
16+
truevaluepoly = 0.94031597258
17+
truevaluepoly_caputo = 1.50450555 # 8 / (3 * np.sqrt(np.pi))
18+
truevaluepoly_caputo_higher = 2 / Gamma(1.5)
19+
20+
# Get SQRT results for checking accuracy.
21+
GL_r = GL(0.5, lambda x: np.sqrt(x), 0, 1, test_N)
22+
GL_result = GL_r[-1]
23+
GL_length = len(GL_r)
24+
25+
GLI_r = GLI(0.5, lambda x: np.sqrt(x), 0, 1, test_N)
26+
GLI_result = GLI_r[-1]
27+
GLI_length = len(GLI_r)
28+
29+
RL_r = RL(0.5, lambda x: np.sqrt(x), 0, 1, test_N)
30+
RL_result = RL_r[-1]
31+
RL_length = len(RL_r)
32+
33+
# --- Exponential function ---
34+
alpha_exp = 0.5
35+
groundtruth_exp = MittagLeffler(1, 1 - 0.5, np.array([1]))[0]
36+
37+
# --- Sine function ---
38+
alpha_sin = 0.5
39+
groundtruth_sin = np.sin(1 + alpha_sin * np.pi / 2)
40+
41+
#######################
42+
# GLpoint
43+
#######################
44+
45+
def test_GLpoint_sqrt_accuracy():
46+
assert abs(GLpoint(0.5, lambda x: x**0.5, 0.0, 1.0, 1024) - sqrtpi2) <= 1e-3
47+
48+
def test_GLpoint_accuracy_polynomial():
49+
assert abs(GLpoint(0.5, lambda x: x**2 - 1, 0.0, 1.0, 1024) - truevaluepoly) <= 1e-3
50+
51+
def test_GLpoint_accuracy_exp():
52+
val = GLpoint(alpha_exp, np.exp, 0, 1, 1024)
53+
assert abs(val - groundtruth_exp) < 1e-3
54+
55+
#######################
56+
# GL
57+
#######################
58+
59+
def test_GL_accuracy_sqrt():
60+
assert abs(GL_result - sqrtpi2) <= 1e-4
61+
62+
def test_GL_accuracy_polynomial():
63+
assert abs(GL(0.5, lambda x: x**2 - 1, 0.0, 1.0, 1024)[-1] - truevaluepoly) <= 1e-3
64+
65+
def test_GL_accuracy_exp():
66+
val = GL(alpha_exp, np.exp, 0, 1, 1024)[-1]
67+
assert abs(val - groundtruth_exp) < 1e-3
68+
69+
#######################
70+
# GLI
71+
#######################
72+
73+
def test_GLI_accuracy_sqrt():
74+
assert abs(GLI_result - sqrtpi2) <= 1e-4
75+
76+
def test_GLI_accuracy_polynomial():
77+
assert abs(GLI(0.5, lambda x: x**2 - 1, 0.0, 1.0, 1024)[-1] - truevaluepoly) <= 6e-3 # low accuracy
78+
79+
def test_GLI_accuracy_exp():
80+
val = GLI(alpha_exp, np.exp, 0, 1, 1024)[-1]
81+
assert abs(val - groundtruth_exp) < 5e-3 # low accuracy
82+
83+
#######################
84+
# RLpoint
85+
#######################
86+
87+
def test_RLpoint_sqrt_accuracy():
88+
assert abs(RLpoint(0.5, lambda x: x**0.5, 0.0, 1.0, 1024) - sqrtpi2) <= 1e-3
89+
90+
def test_RLpoint_accuracy_polynomial():
91+
assert abs(RLpoint(0.5, lambda x: x**2 - 1, 0.0, 1.0, 1024) - truevaluepoly) <= 1e-2
92+
93+
def test_RLpoint_accuracy_exp():
94+
val = RLpoint(alpha_exp, np.exp, 0, 1, 1024)
95+
assert abs(val - groundtruth_exp) < 1e-3
96+
97+
#######################
98+
# RL
99+
#######################
100+
101+
def test_RL_accuracy_sqrt():
102+
assert abs(RL_result - sqrtpi2) <= 1e-4
103+
104+
def test_RL_accuracy_polynomial():
105+
assert abs(RL(0.5, lambda x: x**2 - 1, 0.0, 1.0, 1024)[-1] - truevaluepoly) <= 1e-3
106+
107+
def test_RL_accuracy_exp():
108+
val = RL(alpha_exp, np.exp, 0, 1, 1024)[-1]
109+
assert abs(val - groundtruth_exp) < 1e-3
110+
111+
#######################
112+
# Caputo 1p
113+
#######################
114+
115+
def test_CaputoL1point_accuracy_sqrt():
116+
assert abs(CaputoL1point(0.5, lambda x: x**0.5, 0, 1.0, 1024) - sqrtpi2) <= 1e-2
117+
118+
def test_CaputoL1point_accuracy_polynomial():
119+
assert abs(CaputoL1point(0.5, lambda x: x**2 - 1, 0, 1.0, 1024) - truevaluepoly_caputo) <= 1e-3
120+
121+
def test_CaputoL1point_accuracy_exp():
122+
val = CaputoL1point(alpha_exp, np.exp, 0, 1, 1024)
123+
assert abs(val - groundtruth_exp) < 0.6 # really bad accuracy
124+
125+
#######################
126+
# Caputo 2p
127+
#######################
128+
129+
def test_CaputoL2point_accuracy_polynomial():
130+
assert abs(CaputoL2point(1.5, lambda x: x**2, 0, 1.0, 1024) - truevaluepoly_caputo_higher) <= 1e-1
131+
132+
#######################
133+
# Caputo 2pC
134+
#######################
135+
136+
def test_CaputoL2Cpoint_accuracy_polynomial_higher():
137+
assert abs(CaputoL2Cpoint(1.5, lambda x: x**2, 0, 1.0, 1024) - truevaluepoly_caputo_higher) <= 1e-1
138+
139+
def test_CaputoL2Cpoint_accuracy_polynomial():
140+
assert abs(CaputoL2Cpoint(0.5, lambda x: x**2, 0, 1.0, 1024) - truevaluepoly_caputo) <= 1e-3
141+
142+
def test_CaputoL2Cpoint_accuracy_exp():
143+
val = CaputoL2Cpoint(alpha_exp, np.exp, 0, 1, 1024)
144+
assert abs(val - groundtruth_exp) < 2 # unusable
145+
146+
#######################
147+
# General algorithm output checks
148+
#######################
149+
150+
def test_GL_result_length():
151+
assert GL_length == test_N
152+
153+
def test_GLI_result_length():
154+
assert GLI_length == test_N
155+
156+
def test_RL_result_length():
157+
assert RL_length == test_N
158+
159+
def test_RL_matrix_shape():
160+
assert np.shape(RLmatrix(0.4, test_N)) == (test_N, test_N)
161+
162+
def test_GL_binomial_coefficient_array_size():
163+
assert len(GLcoeffs(0.5, size_coefficient_array)) - 1 == size_coefficient_array
164+
165+
# Optional: Run doctest if called directly
166+
if __name__ == "__main__":
167+
import doctest
168+
doctest.testmod()

0 commit comments

Comments
 (0)