Skip to content

Commit 0739068

Browse files
authored
Merge pull request #92 from Axect/release
Ver 0.40.1
2 parents 23b7a3a + 0692f99 commit 0739068

7 files changed

Lines changed: 105 additions & 28 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "peroxide"
3-
version = "0.40.0"
3+
version = "0.40.1"
44
authors = ["axect <axect@outlook.kr>"]
55
edition = "2018"
66
description = "Rust comprehensive scientific computation library contains linear algebra, numerical analysis, statistics and machine learning tools with farmiliar syntax"
@@ -39,9 +39,8 @@ peroxide-num = "0.1"
3939
anyhow = "1.0"
4040
paste = "1.0"
4141
netcdf = { version = "0.7", optional = true, default-features = false }
42-
pyo3 = { version = "0.22", optional = true, features = [
42+
pyo3 = { version = "0.27.1", optional = true, features = [
4343
"auto-initialize",
44-
"gil-refs",
4544
] }
4645
blas = { version = "0.22", optional = true }
4746
lapack = { version = "0.19", optional = true }

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ How's that? Let me know if there's anything else you'd like me to improve!
321321

322322
## Latest README version
323323

324-
Corresponding to `0.38.0`
324+
Corresponding to `0.40.1`
325325

326326
## Pre-requisite
327327

@@ -351,6 +351,7 @@ cargo add peroxide --features "<FEATURES>"
351351
* `csv`: Adds CSV support for DataFrame
352352
* `parquet`: Adds Parquet support for DataFrame
353353
* `serde`: Enables serialization/deserialization for Matrix and polynomial
354+
* `rkyv`: Enables zero-copy serialization/deserialization with [rkyv](https://rkyv.org)
354355

355356
### Install Examples
356357

RELEASES.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
# Release 0.40.1 (2026-02-06)
2+
3+
## Bug Fixes & Improvements
4+
5+
- Fix numerical instability in RREF (Reduced Row Echelon Form) by comparing to epsilon instead of zero ([#90](https://github.com/Axect/Peroxide/pull/90)) (Thanks to [@developing-human](https://github.com/developing-human))
6+
- Update `pyo3` dependency to 0.27.1 for `plot` feature compatibility ([#89](https://github.com/Axect/Peroxide/pull/89)) (Thanks to [@JSorngard](https://github.com/JSorngard))
7+
- Fix adaptive step size control exponent for embedded Runge-Kutta methods
8+
- Add `order()` method to `ButcherTableau` trait for correct exponent `1/(p+1)`
9+
- BS23: `1/3`, RKF45/DP45/TSIT45: `1/5`, RKF78: `1/8`
10+
- Fix misleading comments on RKF78 BU/BE coefficients
11+
112
# Release 0.40.0 (2025-07-24)
213

314
## Move from `arrow2` to `arrow` & `parquet`

src/numerical/ode.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,12 @@ pub trait ButcherTableau {
290290
fn max_step_iter(&self) -> usize {
291291
unimplemented!()
292292
}
293+
294+
/// Order of the lower-order solution for adaptive step size control.
295+
/// The exponent `1/(order+1)` is used in the step size formula.
296+
fn order(&self) -> usize {
297+
4
298+
}
293299
}
294300

295301
impl<BU: ButcherTableau> ODEIntegrator for BU {
@@ -324,7 +330,7 @@ impl<BU: ButcherTableau> ODEIntegrator for BU {
324330
error = error.max(dt * s.abs())
325331
}
326332

327-
let factor = (self.tol() / error).powf(0.2);
333+
let factor = (self.tol() / error).powf(1.0 / (self.order() as f64 + 1.0));
328334
let new_dt = self.safety_factor() * dt * factor;
329335
let new_dt = new_dt.clamp(self.min_step_size(), self.max_step_size());
330336

@@ -558,6 +564,9 @@ impl ButcherTableau for BS23 {
558564
fn max_step_iter(&self) -> usize {
559565
self.max_step_iter
560566
}
567+
fn order(&self) -> usize {
568+
2
569+
}
561570
}
562571

563572
/// Runge-Kutta-Fehlberg 4/5th order integrator.
@@ -1098,9 +1107,7 @@ impl ButcherTableau for RKF78 {
10981107
],
10991108
];
11001109

1101-
// Coefficients for the 7th order solution (propagated solution)
1102-
// BU_i = BE_i (8th order) - ErrorCoeff_i
1103-
// ErrorCoeff_i = [-41/840, 0, ..., 0, -41/840 (for k11), 41/840 (for k12), 41/840 (for k13)]
1110+
// Coefficients for the 8th order solution (propagated via local extrapolation)
11041111
const BU: &'static [f64] = &[
11051112
0.0,
11061113
0.0,
@@ -1117,8 +1124,8 @@ impl ButcherTableau for RKF78 {
11171124
41.0 / 840.0,
11181125
];
11191126

1120-
// Coefficients for the 8th order solution (used for error estimation)
1121-
// These are from the y[i+1] formula in the MATLAB description
1127+
// Synthetic coefficients for error estimation
1128+
// BU - BE yields the Fehlberg error formula: 41/840 * (k1 + k11 - k12 - k13)
11221129
const BE: &'static [f64] = &[
11231130
41.0 / 840.0,
11241131
0.0,
@@ -1150,6 +1157,9 @@ impl ButcherTableau for RKF78 {
11501157
fn max_step_iter(&self) -> usize {
11511158
self.max_step_iter
11521159
}
1160+
fn order(&self) -> usize {
1161+
7
1162+
}
11531163
}
11541164

11551165
// ┌─────────────────────────────────────────────────────────┐

src/structure/matrix.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3293,14 +3293,17 @@ impl LinearAlgebra<Matrix> for Matrix {
32933293
///
32943294
/// Implementation of [RosettaCode](https://rosettacode.org/wiki/Reduced_row_echelon_form)
32953295
fn rref(&self) -> Matrix {
3296+
let max_abs = self.data.iter().fold(0f64, |acc, &x| acc.max(x.abs()));
3297+
let epsilon = (max_abs * 1e-12).max(1e-15);
3298+
32963299
let mut lead = 0usize;
32973300
let mut result = self.clone();
32983301
'outer: for r in 0..self.row {
32993302
if self.col <= lead {
33003303
break;
33013304
}
33023305
let mut i = r;
3303-
while result[(i, lead)] == 0f64 {
3306+
while result[(i, lead)].abs() < epsilon {
33043307
i += 1;
33053308
if self.row == i {
33063309
i = r;
@@ -3314,7 +3317,7 @@ impl LinearAlgebra<Matrix> for Matrix {
33143317
result.swap(i, r, Row);
33153318
}
33163319
let tmp = result[(r, lead)];
3317-
if tmp != 0f64 {
3320+
if tmp.abs() > epsilon {
33183321
unsafe {
33193322
result.row_mut(r).iter_mut().for_each(|t| *(*t) /= tmp);
33203323
}

src/util/plot.rs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,14 @@
7777
//! - `savefig` : Save plot with given path
7878
7979
extern crate pyo3;
80-
use self::pyo3::types::IntoPyDict;
80+
use self::pyo3::types::{IntoPyDict, PyDictMethods};
8181
use self::pyo3::{PyResult, Python};
8282
pub use self::Grid::{Off, On};
8383
use self::PlotOptions::{Domain, Images, Pairs, Path};
8484
use std::collections::HashMap;
8585
use std::fmt::Display;
86+
use std::borrow::BorrowMut;
87+
use std::ffi::CString;
8688

8789
type Vector = Vec<f64>;
8890

@@ -463,7 +465,7 @@ impl Plot for Plot2D {
463465
}
464466

465467
// Plot
466-
Python::with_gil(|py| {
468+
Python::attach(|py| {
467469
// Input data
468470
let x = self.domain.clone();
469471
let ys = self.images.clone();
@@ -502,24 +504,24 @@ impl Plot for Plot2D {
502504
let plot_type = self.plot_type.clone();
503505

504506
// Global variables to plot
505-
let globals =
506-
vec![("plt", py.import_bound("matplotlib.pyplot")?)].into_py_dict_bound(py);
507-
globals.as_gil_ref().set_item("x", x)?;
508-
globals.as_gil_ref().set_item("y", ys)?;
509-
globals.as_gil_ref().set_item("pair", pairs)?;
510-
globals.as_gil_ref().set_item("n", y_length)?;
511-
globals.as_gil_ref().set_item("p", pair_length)?;
507+
let mut globals =
508+
vec![("plt", py.import("matplotlib.pyplot")?)].into_py_dict(py)?;
509+
globals.borrow_mut().set_item("x", x)?;
510+
globals.borrow_mut().set_item("y", ys)?;
511+
globals.borrow_mut().set_item("pair", pairs)?;
512+
globals.borrow_mut().set_item("n", y_length)?;
513+
globals.borrow_mut().set_item("p", pair_length)?;
512514
if let Some(fs) = fig_size {
513-
globals.as_gil_ref().set_item("fs", fs)?;
515+
globals.borrow_mut().set_item("fs", fs)?;
514516
}
515-
globals.as_gil_ref().set_item("dp", dpi)?;
516-
globals.as_gil_ref().set_item("gr", grid)?;
517-
globals.as_gil_ref().set_item("pa", path)?;
517+
globals.borrow_mut().set_item("dp", dpi)?;
518+
globals.borrow_mut().set_item("gr", grid)?;
519+
globals.borrow_mut().set_item("pa", path)?;
518520
if let Some(xl) = self.xlim {
519-
globals.as_gil_ref().set_item("xl", xl)?;
521+
globals.borrow_mut().set_item("xl", xl)?;
520522
}
521523
if let Some(yl) = self.ylim {
522-
globals.as_gil_ref().set_item("yl", yl)?;
524+
globals.borrow_mut().set_item("yl", yl)?;
523525
}
524526

525527
// Plot Code
@@ -702,7 +704,7 @@ impl Plot for Plot2D {
702704
plot_string.push_str(&format!("plt.savefig(pa, dpi={})", dpi)[..]);
703705
}
704706

705-
py.run_bound(&plot_string[..], Some(&globals), None)?;
707+
py.run(&CString::new(plot_string)?, Some(&globals), None)?;
706708
Ok(())
707709
})
708710
}

tests/matrix.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,54 @@ fn test_kronecker() {
111111
let c1 = a1.kronecker(&b1);
112112
assert_eq!(c1, ml_matrix("0 5 0 10;6 7 12 14;0 15 0 20;18 21 24 28"));
113113
}
114+
115+
#[test]
116+
fn test_rref() {
117+
let a = ml_matrix(
118+
r#"
119+
-3 2 -1 -1;
120+
6 -6 7 -7;
121+
3 -4 4 -6"#,
122+
);
123+
let b = a.rref();
124+
125+
assert_eq!(
126+
b,
127+
ml_matrix(
128+
r#"
129+
1 0 0 2;
130+
0 1 0 2;
131+
0 0 1 -1"#
132+
)
133+
);
134+
}
135+
136+
#[test]
137+
fn test_rref_unstable() {
138+
let epsilon = 1e-10;
139+
140+
// this matrix used to become unstable during rref
141+
let a = ml_matrix(
142+
r#"
143+
1 1 0 0 0 1 0 1 31;
144+
1 1 1 1 0 0 1 1 185;
145+
0 0 1 0 0 1 1 1 165;
146+
1 0 1 0 1 1 0 1 32;
147+
1 0 1 0 0 0 1 1 174;
148+
0 0 1 0 1 1 1 1 171;
149+
0 1 1 0 1 1 0 1 27;
150+
1 0 0 1 0 1 0 0 20;
151+
1 0 1 1 0 1 0 0 23"#,
152+
);
153+
154+
let b = a.rref();
155+
156+
// creating a row like "0 0 0 0 0 0 0 0 1" will "prove" 0 == 1
157+
// which is a tell of numeric instability
158+
for row in 0..b.row {
159+
let ends_in_1 = (b[(row, b.col - 1)] - 1.0).abs() < epsilon;
160+
let rest_zeroes = (0..b.col - 1).all(|col| b[(row, col)].abs() < epsilon);
161+
162+
assert!(!(ends_in_1 && rest_zeroes));
163+
}
164+
}

0 commit comments

Comments
 (0)