Skip to content

Commit 1641e2e

Browse files
authored
Merge pull request #7 from Aschii85/refactor-and-function-usage
Refactor Coefficient Functions & Performance Optimization (LazyFrame)
2 parents 54e8b6e + da52b52 commit 1641e2e

File tree

16 files changed

+639
-395
lines changed

16 files changed

+639
-395
lines changed

Cargo.toml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sde-sim-rs"
3-
version = "0.4.0"
3+
version = "0.5.0"
44
edition = "2024"
55
authors = ["Alexander Schierbeck-Hansen <aschii85@protonmail.com>"]
66
description = "Powerful and flexible stochastic differential equation (quasi) Monte-Carlo simulation library written in Rust with Python bindings"
@@ -17,12 +17,18 @@ crate-type = ["cdylib", "rlib"]
1717
[dependencies]
1818
fasteval = "0.2.4"
1919
lazy_static = "1.5.0"
20+
lru = "0.16.3"
21+
nom = "8.0.0"
2022
ordered-float = "4.2"
21-
polars = "0.49.1"
22-
pyo3 = { version = "0.25.0", features = ["extension-module"] }
23-
pyo3-polars = "0.22.0"
23+
polars = { version = "0.51.0", features = ["diagonal_concat", "lazy"] }
24+
pyo3 = { version = "0.25.1", features = ["auto-initialize"], optional = true }
25+
pyo3-polars = { version = "0.24.0", optional = true }
2426
rand = "0.9.2"
2527
rand_chacha = "0.9.0"
2628
rayon = "1.11.0"
2729
regex = "1.11.1"
2830
sobol = "1.0.2"
31+
32+
[features]
33+
default = []
34+
python = ["dep:pyo3", "dep:pyo3-polars", "pyo3/extension-module"]

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Requires Python version >=3.11,<3.14.
3232
To build the package locally for, you'll first need to compile the Rust package for local development. The project is set up to use `maturin` and `uv`. This command builds the Rust library and creates a Python wheel that can be used directly in your environment.
3333

3434
```
35-
maturin develop --uv --release
35+
maturin develop --uv --release --features python
3636
```
3737

3838
After the compilation is complete, you can run the example to see how the library works. This command uses `uv` to execute the Python example script.

examples/example.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,23 @@
33
import plotly.express as px
44
import polars as pl
55

6-
print(dir(sde_sim_rs))
7-
86

97
def main():
8+
initial_values = {"X0": 0.5, "X1": 100.0, "X2": 0.0}
109
df: pl.DataFrame = sde_sim_rs.simulate(
1110
processes_equations=[
12-
"dX1 = ( sin(t) ) * dt",
13-
"dX2 = (0.01 * X1) * dW1",
14-
"dX3 = (0.005 * X3) * dt + (0.01 * X3) * dW2 + (0.1 * X2 * X3) * dJ1(0.01)",
11+
"dX0 = ( 2.0 * (0.5 - X0) ) * dt + ( 0.1 ) * dN1(X0)",
12+
"dX1 = ( 0.05 * X1 ) * dt + ( 0.2 * X1 ) * dW1 + ( 0.5 ) * dN1(X0)",
13+
"X2 = max(X1 - 100.0, 0.0)",
1514
],
16-
time_steps=list(np.arange(0.0, 100.0, 0.1)),
17-
scenarios=1000,
18-
initial_values={"X1": 0.0, "X2": 1.0, "X3": 100.0},
15+
time_steps=list(np.arange(0.0, 10.0, 0.01)),
16+
scenarios=10_000,
17+
initial_values=initial_values,
1918
rng_method="pseudo",
20-
scheme="euler",
19+
scheme="runge-kutta",
2120
)
2221
print(df)
23-
for i in range(1, 4):
22+
for i in range(0, len(initial_values)):
2423
fig = px.line(
2524
df.filter(pl.col("process_name") == f"X{i}"),
2625
x="time",
@@ -30,6 +29,17 @@ def main():
3029
title="Simulated SDE Process",
3130
)
3231
fig.show()
32+
for i in range(0, len(initial_values)):
33+
fig = px.line(
34+
df.filter(pl.col("process_name") == f"X{i}")
35+
.group_by("time")
36+
.agg(pl.col("value").mean())
37+
.sort("time"),
38+
x="time",
39+
y="value",
40+
title=f"Mean Simulated SDE Process for X{i}",
41+
)
42+
fig.show()
3343

3444

3545
if __name__ == "__main__":

examples/example.rs

Lines changed: 46 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,56 @@
11
use ordered_float::OrderedFloat;
2-
use polars::prelude::*;
3-
use std::collections::HashMap;
4-
use std::time::Instant;
5-
6-
use sde_sim_rs::filtration::Filtration;
72
use sde_sim_rs::proc::util::parse_equations;
83
use sde_sim_rs::sim::simulate;
4+
use std::collections::HashMap;
5+
use std::time::Instant;
96

107
fn main() {
11-
// Simulation Parameters
12-
let dt: f64 = 0.1;
13-
let t_start: f64 = 0.0;
14-
let t_end: f64 = 100.0;
15-
let scenarios: i32 = 10000;
16-
let initial_values = HashMap::from([("X1".to_string(), 1.0), ("X2".to_string(), 1.0)]);
17-
let equations = [
18-
"dX1 = (0.005 * X1) * dt + (0.02 * X1) * dW1".to_string(),
19-
"dX2 = (0.005 * X2) * dt + (0.02 * X1) * dW1 + (0.01 * X2) * dW2 + (1) * dJ1(0.5)"
20-
.to_string(),
8+
// ────── configuration ──────
9+
let initial_values = HashMap::from([("X1".to_string(), 100.0), ("X2".to_string(), 0.0)]);
10+
11+
let processes_equations = vec![
12+
"dX1 = ( sin(t) ) * dt + (0.01 * X1) * dW1 + (0.001 * X1) * dN1(0.5 * cos(t))".to_string(),
13+
"X2 = max(X1 - 100.0, 0.0)".to_string(),
2114
];
22-
let scheme = "runge-kutta"; // "euler" or "runge-kutta"
23-
let rng_method = "sobol"; // "pseudo" or "sobol"
2415

25-
// 1. Prepare Time Steps
26-
let time_steps: Vec<OrderedFloat<f64>> = (0..)
27-
.map(|i| OrderedFloat(t_start + i as f64 * dt))
28-
.take_while(|t| t.0 <= t_end)
16+
let scheme = "euler"; // other valid value: "runge-kutta"
17+
let rng_method = "pseudo"; // other valid value: "sobol"
18+
let scenarios: u64 = 10_000;
19+
20+
// build a uniformly spaced time vector, identical to what the Python
21+
// wrapper accepts as `time_steps: Vec<f64>`.
22+
let dt = 0.1;
23+
let t_start = 0.0;
24+
let t_end = 100.0;
25+
let time_steps: Vec<f64> = (0..)
26+
.map(|i| t_start + i as f64 * dt)
27+
.take_while(|t| *t <= t_end)
2928
.collect();
3029

31-
// 2. Parse equations
32-
let universe =
33-
parse_equations(&equations, time_steps.clone()).expect("Failed to parse equations");
34-
35-
// 3. Initialize Filtration
36-
let mut filtration = Filtration::new(
37-
universe,
38-
time_steps.clone(),
39-
(1..=scenarios).collect(),
40-
Some(initial_values),
41-
);
42-
43-
// Run Simulation
44-
let before = Instant::now();
45-
println!("Starting simulation with {} RNG...", rng_method);
46-
simulate(&mut filtration, scheme, rng_method);
47-
48-
let duration = before.elapsed();
49-
println!(
50-
"Simulation completed in {:.4} seconds.\n",
51-
duration.as_secs_f64()
52-
);
53-
54-
let df: DataFrame = filtration.to_dataframe();
55-
println!("{}", df);
56-
57-
assert!(duration.as_secs_f64() > 0.0);
30+
// convert the floats to `OrderedFloat` for internal use
31+
let ordered_steps: Vec<OrderedFloat<f64>> =
32+
time_steps.iter().copied().map(OrderedFloat).collect();
33+
34+
// parse the equations into a ProcessUniverse (same work done in Python)
35+
let universe = parse_equations(&processes_equations, ordered_steps.clone())
36+
.expect("failed to parse process equations");
37+
38+
// run the actual simulation; this mirrors the body of `simulate_py`
39+
let start = Instant::now();
40+
println!("running {} scenarios with {} rng...", scenarios, rng_method);
41+
let lf = simulate(
42+
&universe,
43+
ordered_steps.clone(),
44+
initial_values.clone(),
45+
scenarios,
46+
scheme,
47+
rng_method,
48+
)
49+
.expect("failed to run simulation");
50+
let df = lf.collect().expect("failed to collect results");
51+
let elapsed = start.elapsed();
52+
println!("completed in {:.3}s", elapsed.as_secs_f64());
53+
54+
// print a small portion of the output frame
55+
println!("{:#?}", df.head(Some(10)));
5856
}

pyproject.toml

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

55
[project]
66
name = "sde-sim-rs"
7-
requires-python = ">=3.11,<3.14"
7+
requires-python = ">=3.11,<3.15"
88
authors = [
99
{name = "Alexander Schierbeck-Hansen", email = "aschii85@protonmail.com"},
1010
]

python/sde_sim_rs/__init__.pyi

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@ from typing import Literal
33

44
import polars as pl
55

6-
76
def simulate(
87
processes_equations: Sequence[str],
98
time_steps: Sequence[float],
109
scenarios: int,
1110
initial_values: Mapping[str, float],
1211
rng_method: Literal["pseudo", "sobol"] = "pseudo",
1312
scheme: Literal["euler", "runge-kutta"] = "euler",
14-
) -> pl.DataFrame:
13+
) -> pl.DataFrame:
1514
"""
1615
Simulates stochastic differential equations (SDEs) using the specified methods.
1716
@@ -45,12 +44,14 @@ def simulate(
4544
method. Defaults to "euler".
4645
4746
Returns:
48-
A Polars DataFrame containing the simulated values. The DataFrame has a
49-
`time` column and columns for each process, with each row corresponding
50-
to a specific time step and scenario.
47+
A Polars DataFrame containing the simulated values. The DataFrame is
48+
"long"/tidy: every row represents a single `(scenario, time, process)`
49+
triple and the associated value. In other words, the `scenario`
50+
dimension has already been appended, which makes it easy to group or
51+
aggregate across paths using standard Polars operations.
5152
5253
Raises:
5354
ValueError: If the process equations are malformed or if initial values
5455
are missing for any process.
5556
"""
56-
...
57+
...

0 commit comments

Comments
 (0)