Skip to content

Commit

Permalink
ENH: add rust Dual and Spline as extension modules. (#136)
Browse files Browse the repository at this point in the history
Co-authored-by: JHM Darbyshire (iMac) <[email protected]>
  • Loading branch information
attack68 and attack68 authored Mar 28, 2024
1 parent f690068 commit 7103c54
Show file tree
Hide file tree
Showing 70 changed files with 6,019 additions and 360 deletions.
12 changes: 4 additions & 8 deletions .github/workflows/ubuntu-latest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,17 @@ jobs:
python-version: ["3.11"]

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-latest.txt
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
pip install .[dev] -v
- name: Test with pytest
run: |
coverage run -m pytest
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/ubuntu-minimum.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ jobs:
python-version: ["3.9"]

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
Expand All @@ -28,6 +30,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -r requirements-minimum.txt
pip install . -v
- name: Test with pytest
run: |
pytest
12 changes: 4 additions & 8 deletions .github/workflows/windows-latest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,17 @@ jobs:
python-version: ["3.11"]

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-latest.txt
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
pip install .[dev] -v
- name: Test with pytest
run: |
pytest
5 changes: 4 additions & 1 deletion .github/workflows/windows-minimum.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ jobs:
python-version: ["3.9"]

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
Expand All @@ -28,6 +30,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -r requirements-minimum.txt
pip install . -v
- name: Test with pytest
run: |
pytest
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ __pycache__/
# C extensions
*.so

# Rust extensions
src/bin

# Distribution / packaging
.Python
local_resources/
Expand Down Expand Up @@ -138,4 +141,5 @@ dmypy.json
scratch.py

.idea/
/*.ipynb
/*.ipynb
Carlo.lock
6 changes: 5 additions & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ build:
os: ubuntu-22.04
tools:
python: "3.11"
rust: "1.75"
# You can also specify other tool versions:
# nodejs: "19"
# rust: "1.64"
Expand All @@ -28,4 +29,7 @@ sphinx:
# Optionally declare the Python requirements required to build your docs
python:
install:
- requirements: requirements.txt
- method: pip
path: .
extra_requirements:
- dev
39 changes: 39 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[package]
name = "rateslibrs"
version = "1.2.0"
edition = "2021"

[lib]
name = "rateslibrs"
path = "src/lib.rs"
#crate-type = ["rlib"]
#crate-type = ["cdylib"] # for pyO3
crate-type = ["lib"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
#pyo3 = { version = "0.20.3", features = ["abi3-py39", "extension-module"] }
#chrono = "0.4.0"
ndarray = "0.15.6"
indexmap = "1.9.3"
num-traits = "0.2.15"
auto_ops = "0.3.0"
numpy = "0.20.0"
itertools = "0.12.1"

# --- This section should be live in development to use `cargo test --lib --no-default-features`
[dependencies.pyo3]
version = "0.20.3"

[features]
extension-module = ["pyo3/extension-module"]
default = ["extension-module"]
# ------------- When building commment this out and use the abi3-py39 feature in the above block.

[dev-dependencies]
criterion = { version = "0.4", features = ["html_reports"] }

[[bench]]
name = "my_benchmark"
harness = false
4 changes: 3 additions & 1 deletion PACKAGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ On "main":
1) Update the whatsnew with the target release date.
2) Add a new entry to the switcher.json in main:docs/source/static, pushing stable to next version.
3) Change the badges.json file is there is anything to add, e.g. versions.
4) Bump the "version" in pyproject.toml and check the dependencies.
4) Bump the "version" in pyproject.toml, and __init__ __version__ and check the dependencies.
5) Checks should be OK in github actions but perform a local double check.

Checks:
Expand All @@ -26,6 +26,8 @@ On "release branch":
DEVELOPMENT to False.

4) Commit and Push the branch.
5) Run `cargo test --lib --no-default-features`
6) Change the Cargo.toml file for abi3-py39 features.

Build:
$ pip install build twine
Expand Down
2 changes: 2 additions & 0 deletions benches/_README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Contains benchmark files for rust implementation.
Directed by Cargo.toml.
95 changes: 95 additions & 0 deletions benches/my_benchmark.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};

use rateslibrs::dual::dual1::Dual;
use rateslibrs::dual::Duals;

use indexmap::set::IndexSet;
use ndarray::Array;
use std::rc::Rc;

fn dual_add_bm(a: &Dual, b: &Dual) -> Dual {
a + b
}

fn duals_add_bm(a: &Duals, b: &Duals) -> Duals {
a + b
}

fn dual_sub_bm(a: &Dual, b: &Dual) -> Dual {
a - b
}

fn dual_mul_bm(a: &Dual, b: &Dual) -> Dual {
a * b
}

fn dual_div_bm(a: &Dual, b: &Dual) -> Dual {
a / b
}

fn float_add_bm(a: &f64, b: &f64) -> f64 {
a + b
}

fn criterion_benchmark(c: &mut Criterion) {
let dual_ = Array::ones(1000);
let vars = IndexSet::from_iter((0..1000).map(|x| format!("v{}", x).to_string()));
let dual_2 = Array::ones(1000);
let vars2 = IndexSet::from_iter((0..1000).map(|x| format!("u{}", x).to_string()));

let a = Dual {
real: 2.0,
vars: Rc::new(vars),
dual: dual_,
};
let b = Dual {
real: 3.0,
vars: Rc::new(vars2),
dual: dual_2,
};
let x = 20.1;
let y = 22.1;

let duals_ = Array::ones(1000);
let duals_2 = Array::ones(1000);
let vars = IndexSet::from_iter((0..1000).map(|x| format!("v{}", x).to_string()));
let vars2 = IndexSet::from_iter((0..1000).map(|x| format!("u{}", x).to_string()));
let a2 = Duals::Dual(Dual {
real: 2.0,
vars: Rc::new(vars),
dual: duals_,
});
let b2 = Duals::Dual(Dual {
real: 3.0,
vars: Rc::new(vars2),
dual: duals_2,
});
let x2 = Duals::Float(20.1);
let y2 = Duals::Float(22.1);

c.bench_function("float add", |z| z.iter(|| float_add_bm(&x, &y)));
c.bench_function("float add duals", |z| z.iter(|| duals_add_bm(&x2, &y2)));
c.bench_function("dual add diff vars", |z| z.iter(|| dual_add_bm(&a, &b)));
c.bench_function("duals add diff vars", |z| z.iter(|| duals_add_bm(&a2, &b2)));
c.bench_function("dual sub diff vars", |z| z.iter(|| dual_sub_bm(&a, &b)));
c.bench_function("dual mul diff vars", |z| z.iter(|| dual_mul_bm(&a, &b)));
c.bench_function("dual div diff vars", |z| z.iter(|| dual_div_bm(&a, &b)));
c.bench_function("dual add same ptr vars", |z| {
z.iter(|| dual_add_bm(&a, &a.clone()))
});
c.bench_function("duals add same ptr vars", |z| {
z.iter(|| duals_add_bm(&a2, &a2.clone()))
});
c.bench_function("dual sub same ptr vars", |z| {
z.iter(|| dual_sub_bm(&a, &a.clone()))
});
c.bench_function("dual mul same ptr vars", |z| {
z.iter(|| dual_mul_bm(&a, &a.clone()))
});
c.bench_function("dual div same ptr vars", |z| {
z.iter(|| dual_div_bm(&a, &a.clone()))
});
}

criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
44 changes: 44 additions & 0 deletions docs/source/api/rateslib.dual.Dual.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
Dual
==========

.. currentmodule:: rateslib.dual

.. py:class:: Dual(real, vars, dual)
Dual number data type to perform first derivative automatic differentiation.

:param real: The real coefficient of the dual number
:type real: float, int

:param vars: The labels of the variables for which to record derivatives. If empty,
the dual number represents a constant, equivalent to a float.
:type vars: tuple of str

:param dual: First derivative information contained as coefficient of linear manifold.
Defaults to an array of ones the length of ``vars`` if empty.
:type dual: list of float

.. rubric:: Attributes

:ivar real: float
:ivar vars: sequence of str
:ivar dual: 1d ndarray

.. seealso::
:class:`~rateslib.dual.Dual2`: Dual number data type to perform second derivative automatic differentiation.

.. rubric:: Examples

.. ipython:: python
from rateslib.dual import Dual, gradient
def func(x, y):
return 5 * x**2 + 10 * y**3
x = Dual(1.0, ["x"], [])
y = Dual(1.0, ["y"], [])
gradient(func(x,y), ["x", "y"])
.. rubric:: Methods Summary

.. include:: rateslib.dual.Dual.vars_from.rst
44 changes: 44 additions & 0 deletions docs/source/api/rateslib.dual.Dual.vars_from.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.. vars_from
==========
.. .. currentmodule:: rateslib.dual
.. py:method:: Dual.vars_from(other, real, vars, dual)
Create a :class:`~rateslib.dual.Dual` object with ``vars`` linked with another.

:param other: The other Dual from which to link vars.
:type other: Dual

:param real: The real coefficient of the dual number
:type real: float, int

:param vars: The labels of the variables for which to record derivatives. If empty,
the dual number represents a constant, equivalent to a float.
:type vars: tuple of str

:param dual: First derivative information contained as coefficient of linear manifold.
Defaults to an array of ones the length of ``vars`` if empty.
:type dual: list of float

:rtype: Dual

.. rubric:: Notes

Variables are constantly checked when operations are performed between dual numbers. In Rust the variables
are stored within an ARC pointer. It is much faster to check the equivalence of two ARC pointers than if the elements
within a variables Set, say, are the same *and* in the same order. This method exists to create dual data types
with shared ARC pointers directly.

.. ipython:: python
from rateslib import Dual
x1 = Dual(1.0, ["x"], [])
x2 = Dual(2.0, ["x"], [])
# x1 and x2 have the same variables (["x"]) but it is a different object
x1.ptr_eq(x2)
x3 = Dual.vars_from(x1, 3.0, ["x"], [])
# x3 contains shared object variables with x1
x1.ptr_eq(x3)
Loading

0 comments on commit 7103c54

Please sign in to comment.