Skip to main content

An implementation of generalized dual numbers with support for arbitrary precision and vectorization.

Project description

generalized-dual

A minimal Python library for generalized dual numbers and automatic differentiation, supporting arbitrary-order derivatives, complex numbers, and vectorized operations. Many difficult functions are implemented.

Goal

The goal of this project is to create a simple Python library capable of performing multi-variable, high-order automatic differentiation using generalized dual numbers, while supporting real and complex numbers, vectorized operations, and rich function support.

Installation

This package is not yet available on PyPI.

To install it locally, clone the repository and install it using pip:

git clone https://github.com/LukaLavs/Generalized-Dual.git
cd generalized_dual
pip install .

Usage

from generalized_dual import GeneralizedDual, initialize
from generalized_dual.functions import exp, log, sin
import numpy as np
import mpmath

mpmath.mp.dps = 100  # Set precision

x, y = initialize(np.array([0.5]), np.array([1.0]), m=2) # Prepare variables which support order m derivatives.
f = exp(x * y) + log(y) # Define a dual function and evaluate it
print(f.diff((1, 0)))  # Partial derivative f_x^1y^0

Features

  • Generalized dual numbers for multi-variable, high-order differentiation
  • Real and complex support via mpmath
  • Vectorized NumPy operations
  • Symbolic integration (via Taylor expansions)
  • Rich function support: exp, log, dual_abs, trig, gamma, erfinv, lambertw, and more
  • 📖 See the full list of available functions in FUNCTIONS.md.

Advanced Examples

⚙️ Higher-Order Partial Derivatives

from generalized_dual import *
import numpy as np
import mpmath

mpmath.mp.dps = 50 # Set percision
X = np.linspace(0, 3, 5)
Y = np.sin(X)
Z = np.array([2, 7, 2, 3, 1])
# Such choice of X, Y, Z will give us derivatives around points in (X, Y, Z)
x, y, z = initialize(X, Y, Z, m=5) # Set max order of terms to 5, since later we calculate f_x^2y_1z_2 which is of order 5
F = lambertw(x * y - nroot(z, 6)) * log(x, y)
Df = F.diff((2, 1, 2))
print("F_x^2yz^2(X, Y, Z) = ")
print(Df)

🧮 Approximate 2D Integral via Taylor Expansion

from generalized_dual import *
import numpy as np
import mpmath

mpmath.mp.dps = 15
A = [0.3, 0.2] # A = [x_min, y_min]
B = [0.7, 0.6] # B = [x_max, y_max]
N = [20, 20] # Subdivide x interval into 20 pieces, and same for y interval
m = 2 # Approximate with multiple order 2 polinomials around points defined by A;B;N triple
integrand = lambda X, Y: sin(X * Y) / beta(X, Y)
# Now approximate \int_A^B integrand(x, y) dydx
integral_aprox = experimental.integrate(integrand, A, B, N, m)
print("Integral approximation: ")
print(integral_aprox)

🔁 Define a Custom Inverse Function via Lagrange Inversion

from generalized_dual import *
import numpy as np
import mpmath

mpmath.mp.dps = 20
def my_inverse_func(X):
    f0, f_hat = X._decompose() # Decompose X into dual and non-dual part
    f0 = np.vectorize(lambda x: mpmath.erfinv(x) if -1 <= x <= 1 else mpmath.nan)(f0) # Apply inverse f on non-dual part
    X = GeneralizedDual._compose(f0, f_hat) # Compose it back
    x0 = initialize(f0, m=X.m) # Initialize a helper variable
    F = erf(x0) # evaluate dual f function
    df = F.derivatives_along(0) # Obtain its derivatives (gives [f^0, f^1, ..., f^m])
    return inverse(X, df) # Calls a function which applies Lagrange Inversion Theorem

x = initialize(0.4, m=7)
F = my_inverse_func(x)
print("List of [f^0, f^1, ..., f^m](X):")
disp(F.derivatives_along(0))

🔍 Access Specific Taylor Term

from generalized_dual import *
import numpy as np

X = np.array([[1, 2], [3, 4]])
Y = np.log(X)
x, y = initialize(X, Y, m=3)
k, l = 2, 1
term = (fresnelc(x - comb(x, y))).terms[(k, l)]
print("The term is: ")
disp(term) # Term next to (x - x0)^k(y - y0)^l
# Note: terms is same size as X and Y variables

📈 Plot Mixed Derivative fx²y² with Custom Function

from generalized_dual import *
import numpy as np
import matplotlib.pyplot as plt
import time

mpmath.mp.dps = 15
X, Y = np.linspace(0, 2, 200), 2.3 * np.ones(200)
x, y = initialize(X, Y, m=4)

F = lambda X, Y: dual_abs(lambertw(X) + log(Y)) * exp(-X**2) + \
                 rising_factorial(sin(X * 5), fresnelc(X * Y) / (X * Y) + 3)

start = time.time()
fx2y2 = F(x, y).diff((2, 2), to_float=True) # to_float=True converts from mpmath objects into floats for easier graphing
end = time.time()

print(f"Execution time for a plot: {end - start:.6f} seconds.")

plt.plot(X, fx2y2, label='∂²f/∂x²∂y² at y=2.3')
plt.title('f(x, y) := abs(lambertw(x) + log(y)) + rising_factorial(...)')
plt.grid(True)
plt.legend()
plt.show()

🔍 Taylor Approximation of |LambertW(sin(xy + z))| on Branch -1

from generalized_dual import *
import numpy as np
import mpmath
import matplotlib.pyplot as plt

X = np.linspace(0, 3, 5)
Y, Z = np.sin(X) + 1, np.cos(X)
x, y, z = initialize(X, Y, Z, m=3)

F = dual_abs(lambertw(sin(x * y + z), branch=-1))
p = 1  # build_taylor will return an array of functions (same size as X, Y and Z)
# With p=1 we will select the function around (x, y, z) = (X[1], Y[1], Z[1])

X_test = np.linspace(-2 * np.pi, 2 * np.pi, 300)
f_exact = lambda x: float(mpmath.fabs(mpmath.lambertw(mpmath.sin(x * Y[p] + Z[p]), k=-1))) # Fix y and z variables to get 1-D function
Y_true = np.vectorize(f_exact)(X_test)

taylf = build_taylor(F, X, Y, Z, to_float=True)[p]
Y_taylor = taylf([X_test, None, None]) # Y and Z variables are fixed. (Fixing variables is not required, but in our case we would get 4-D function or 3-D, which would be hard to plot)

plt.plot(X_test, Y_true, label='True')
plt.plot(X_test, Y_taylor, label='Taylor')
plt.scatter(X[p], f_exact(X[p]), color='red')
plt.title(r"$|W_{-1}(\sin(xy+z))|$ Taylor Approximation")
plt.grid(); plt.legend(); plt.show()

✨ Another plot example

from generalized_dual import *
import numpy as np
import matplotlib.pyplot as plt

X = 2
x = initialize(X, m=10)

F = sin(cos(x**2) + atan(x)) + sin(4*x)
f = lambda x: np.sin(np.cos(x**2) + np.arctan(x)) + np.sin(4*x)

taylor = build_taylor(F, X, to_float=True) # Since X is not an array, build_taylor just returns a function

X_range = np.linspace(0, 3, 100)
plt.plot(X_range, f(X_range), label='func')
plt.plot(X_range, taylor([X_range]), label='taylor')
plt.scatter(X, f(X))
plt.ylim(-3, 3)
plt.legend()
plt.show()

🌐 Visualize 2D Taylor Approximation in 3D

from generalized_dual import *
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D

# Setup
X, Y = 2, 3
x, y = initialize(X, Y, m=3) # With a goal of plotting taylor polinomial around point (x, y) = (2, 3)
F = sin(x * y) # for sin(x y) function
f = lambda x, y: np.sin(x * y) # Define a function for plotting the original
taylor = build_taylor(F, X, Y, to_float=True) # build taylor function around (X, Y)
# If X, Y were ndarrays the result would be a ndarray of functions around respected centers
# Here build_taylor returned just a function (since X, Y are not arrays)

# Grid
x_vals = np.linspace(0, 4, 100)
y_vals = np.linspace(0, 4, 100)
X_grid, Y_grid = np.meshgrid(x_vals, y_vals)

# Evaluate
Z_fun = f(X_grid, Y_grid)
Z_taylor = taylor([X_grid, Y_grid]) # Evaluate taylor polinom around (X, Y)
# Note: taylor([X_grid, None]) would return 1-D taylor polinomial of a function f(x, y0=3) around x0=2
Z_point = f(X, Y)

# Plot
fig = plt.figure(figsize=(10, 6))
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(X_grid, Y_grid, Z_fun, cmap='viridis', alpha=0.5)
ax.plot_surface(X_grid, Y_grid, Z_taylor, cmap='plasma', alpha=0.5)
ax.scatter(X, Y, Z_point, color='red', s=40)

ax.set_title("Function vs. Taylor Approximation")
ax.set_xlabel('x'); ax.set_ylabel('y'); ax.set_zlabel('z')

ax.set_xlim(1.5, 2.5)
ax.set_ylim(2.5, 3.5)
ax.set_zlim(-1, 1)

legend_elements = [
    Line2D([0], [0], color='blue', lw=3, label='Function'),
    Line2D([0], [0], color='orange', lw=3, label='Taylor'),
    Line2D([0], [0], marker='o', color='w', markerfacecolor='red', markersize=8, label='Center')
]
ax.legend(handles=legend_elements, loc='upper left')

plt.tight_layout()
plt.show()

Project Structure

generalized_dual/
│
├── core.py          # GeneralizedDual class
├── functions.py     # Math operations
├── utils.py         # Initialization, display tools
├── experimental/    # Optional features
├── __init__.py
tests/
setup.py
pyproject.toml
README.md
LICENSE

📚 References & Further Reading

This project was inspired and guided by the following resources:

If you found additional valuable resources, please consider contributing!

Contributing & Ideas

Contributions, bug reports, and feature suggestions are very welcome!

  • Feel free to submit a pull request or open an issue on GitHub.
  • You can also share ideas, questions, or suggestions via email: lavsluka@gmail.com.

By contributing, you help improve the library and expand its capabilities for the community.


License

MIT © 2025 Luka Lavš

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gen_dual-0.1.0.tar.gz (42.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gen_dual-0.1.0-py3-none-any.whl (42.8 kB view details)

Uploaded Python 3

File details

Details for the file gen_dual-0.1.0.tar.gz.

File metadata

  • Download URL: gen_dual-0.1.0.tar.gz
  • Upload date:
  • Size: 42.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.4

File hashes

Hashes for gen_dual-0.1.0.tar.gz
Algorithm Hash digest
SHA256 555c4b46dd6a1c119d59ef9fe2680eac4777c9b65cc6aebfeb874e825dc85400
MD5 298e993e7ffc00a5ad8c0c189bc489d3
BLAKE2b-256 ea72b1969215fd3fa36e32e272698c693a8bfa0d9705f6bbe807d764287cd1e0

See more details on using hashes here.

File details

Details for the file gen_dual-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: gen_dual-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 42.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.4

File hashes

Hashes for gen_dual-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1ed604ddb565167f80871d3ce17954c3f87044e69aafb90a66f2a89db2020009
MD5 d37d950d729db0e3cb1f4039a68cd167
BLAKE2b-256 31ee66ee1fad55bd20b48a1490d1dc6af943ed3bacc8ee78a08fd190eda8fe5d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page