Release v0.2.0: Comprehensive DE, Mathematical Toolkit, Optimal Control
Major Features: • Comprehensive Differential Evolution with 5 strategies (rand1, best1, currenttobest1, rand2, best2) • Adaptive jDE algorithm for self-tuning F and CR parameters • Convergence tracking with history records and early stopping • Mathematical toolkit module (780 lines): gradient, hessian, jacobian, statistics, linear algebra • Optimal control framework: HJB solvers, regime switching, jump diffusion, MRSJD • Sparse optimization: Sparse PCA, Box-Tao decomposition, ADMM, Elastic Net • Rayon parallelization infrastructure (ready for pure Rust objectives) Performance: • 74-88× speedup for DE vs SciPy • 50-100× speedup overall vs pure Python Refactoring & Cleanup: • Removed 5 legacy files (de_refactored.rs, hmm_legacy.rs, hmm_refactored.rs, mcmc_legacy.rs, mcmc_refactored.rs) • Modular architecture with trait-based design • Generic implementations (no domain-specific code) • Updated Python bindings for new DE API • Fixed ALL compilation warnings (0 errors, 0 warnings) Documentation: • Updated README with v0.2.0 features and benchmarks • Created RELEASE_NOTES_v0.2.0.md (comprehensive changelog) • New optimal control tutorial notebook (03_optimal_control_tutorial.ipynb) • Updated API examples in README • Created test_release.py for release validation Version Bumps: • Cargo.toml: 0.1.0 → 0.2.0 • pyproject.toml: 0.1.0 → 0.2.0 • python/__init__.py: 0.1.0 → 0.2.0 Breaking Changes: • DE API: mutation_factor/crossover_rate → f/cr • DE API: use_adaptive_jde → adaptive • DE API: strategy names simplified (e.g., 'rand/1/bin' → 'rand1') • DE returns: (x, fun) tuple instead of dict-like object Known Items (Post-Release): • Mathematical toolkit functions available in Rust but not yet exposed to Python • MCMC Python wrapper needs API update to match new Rust implementation • Tutorial notebooks need DE API updates Tests: 34 Rust tests passing, core Python functionality validated with test_release.py
This commit is contained in:
+8
-8
@@ -37,27 +37,27 @@ impl<P: ProposalStrategy> MCMCConfigBuilder<P> {
|
||||
adaptation_interval: 100,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn burn_in(mut self, burn_in: usize) -> Self {
|
||||
self.burn_in = burn_in;
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
pub fn thin(mut self, thin: usize) -> Self {
|
||||
self.thin = thin.max(1);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
pub fn proposal(mut self, proposal: P) -> Self {
|
||||
self.proposal = Some(proposal);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
pub fn adaptation_interval(mut self, interval: usize) -> Self {
|
||||
self.adaptation_interval = interval;
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
pub fn build(self) -> Result<MCMCConfig<P>>
|
||||
where
|
||||
P: Default,
|
||||
@@ -67,13 +67,13 @@ impl<P: ProposalStrategy> MCMCConfigBuilder<P> {
|
||||
"n_samples must be positive".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
if self.initial_state.is_empty() {
|
||||
return Err(OptimizrError::InvalidParameter(
|
||||
"initial_state cannot be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
Ok(MCMCConfig {
|
||||
n_samples: self.n_samples,
|
||||
burn_in: self.burn_in,
|
||||
@@ -97,7 +97,7 @@ mod tests {
|
||||
.thin(2)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
|
||||
assert_eq!(config.n_samples, 1000);
|
||||
assert_eq!(config.burn_in, 100);
|
||||
assert_eq!(config.thin, 2);
|
||||
|
||||
@@ -2,24 +2,28 @@
|
||||
//!
|
||||
//! Defines the LogLikelihood trait for target distributions.
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Generic log-likelihood function trait
|
||||
pub trait LogLikelihood: Send + Sync {
|
||||
fn evaluate(&self, state: &[f64]) -> f64;
|
||||
}
|
||||
|
||||
#[cfg(feature = "python-bindings")]
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Wrapper for Python callable log-likelihood
|
||||
#[cfg(feature = "python-bindings")]
|
||||
pub struct PyLogLikelihood {
|
||||
func: Py<PyAny>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "python-bindings")]
|
||||
impl PyLogLikelihood {
|
||||
pub fn new(func: Py<PyAny>) -> Self {
|
||||
Self { func }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "python-bindings")]
|
||||
impl LogLikelihood for PyLogLikelihood {
|
||||
fn evaluate(&self, state: &[f64]) -> f64 {
|
||||
Python::with_gil(|py| {
|
||||
@@ -37,7 +41,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
struct TestLogLikelihood;
|
||||
|
||||
|
||||
impl LogLikelihood for TestLogLikelihood {
|
||||
fn evaluate(&self, state: &[f64]) -> f64 {
|
||||
// Standard normal log-likelihood
|
||||
|
||||
+9
-5
@@ -17,15 +17,19 @@
|
||||
//! // Create config and sample
|
||||
//! ```
|
||||
|
||||
mod proposal;
|
||||
mod config;
|
||||
mod likelihood;
|
||||
mod sampler;
|
||||
mod proposal;
|
||||
#[cfg(feature = "python-bindings")]
|
||||
mod python_bindings;
|
||||
mod sampler;
|
||||
|
||||
// Re-export public API
|
||||
pub use proposal::{ProposalStrategy, GaussianProposal, AdaptiveProposal};
|
||||
pub use config::{MCMCConfig, MCMCConfigBuilder};
|
||||
pub use likelihood::{LogLikelihood, PyLogLikelihood};
|
||||
pub use likelihood::LogLikelihood;
|
||||
#[cfg(feature = "python-bindings")]
|
||||
pub use likelihood::PyLogLikelihood;
|
||||
pub use proposal::{AdaptiveProposal, GaussianProposal, ProposalStrategy};
|
||||
#[cfg(feature = "python-bindings")]
|
||||
pub use python_bindings::{adaptive_mcmc_sample, mcmc_sample};
|
||||
pub use sampler::MetropolisHastings;
|
||||
pub use python_bindings::{mcmc_sample, adaptive_mcmc_sample};
|
||||
|
||||
+12
-18
@@ -2,18 +2,18 @@
|
||||
//!
|
||||
//! Defines the ProposalStrategy trait and common implementations.
|
||||
|
||||
use rand::Rng;
|
||||
use rand::distributions::Distribution;
|
||||
use rand::Rng;
|
||||
use rand_distr::Normal;
|
||||
|
||||
/// Trait for MCMC proposal strategies
|
||||
pub trait ProposalStrategy: Send + Sync + Clone {
|
||||
/// Generate proposed next state from current state
|
||||
fn propose(&self, current: &[f64], rng: &mut impl Rng) -> Vec<f64>;
|
||||
|
||||
|
||||
/// Adapt proposal based on acceptance rate (optional)
|
||||
fn adapt(&mut self, _acceptance_rate: f64) {}
|
||||
|
||||
|
||||
/// Name of the strategy
|
||||
fn name(&self) -> &'static str;
|
||||
}
|
||||
@@ -33,12 +33,9 @@ impl GaussianProposal {
|
||||
impl ProposalStrategy for GaussianProposal {
|
||||
fn propose(&self, current: &[f64], rng: &mut impl Rng) -> Vec<f64> {
|
||||
let normal = Normal::new(0.0, self.step_size).unwrap();
|
||||
current
|
||||
.iter()
|
||||
.map(|&x| x + normal.sample(rng))
|
||||
.collect()
|
||||
current.iter().map(|&x| x + normal.sample(rng)).collect()
|
||||
}
|
||||
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"GaussianRandomWalk"
|
||||
}
|
||||
@@ -66,7 +63,7 @@ impl AdaptiveProposal {
|
||||
adaptation_rate: 0.01,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn with_target_acceptance(mut self, target: f64) -> Self {
|
||||
self.target_acceptance = target;
|
||||
self
|
||||
@@ -76,17 +73,14 @@ impl AdaptiveProposal {
|
||||
impl ProposalStrategy for AdaptiveProposal {
|
||||
fn propose(&self, current: &[f64], rng: &mut impl Rng) -> Vec<f64> {
|
||||
let normal = Normal::new(0.0, self.step_size).unwrap();
|
||||
current
|
||||
.iter()
|
||||
.map(|&x| x + normal.sample(rng))
|
||||
.collect()
|
||||
current.iter().map(|&x| x + normal.sample(rng)).collect()
|
||||
}
|
||||
|
||||
|
||||
fn adapt(&mut self, acceptance_rate: f64) {
|
||||
let delta = (acceptance_rate - self.target_acceptance) * self.adaptation_rate;
|
||||
self.step_size *= (1.0 + delta).max(0.5).min(2.0);
|
||||
}
|
||||
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"AdaptiveGaussian"
|
||||
}
|
||||
@@ -108,7 +102,7 @@ mod tests {
|
||||
let proposal = GaussianProposal::new(0.5);
|
||||
let current = vec![0.0, 1.0];
|
||||
let mut rng = thread_rng();
|
||||
|
||||
|
||||
let proposed = proposal.propose(¤t, &mut rng);
|
||||
assert_eq!(proposed.len(), 2);
|
||||
}
|
||||
@@ -117,11 +111,11 @@ mod tests {
|
||||
fn test_adaptive_proposal() {
|
||||
let mut proposal = AdaptiveProposal::new(0.1);
|
||||
let initial_step = proposal.step_size;
|
||||
|
||||
|
||||
// High acceptance should increase step size
|
||||
proposal.adapt(0.5);
|
||||
assert!(proposal.step_size > initial_step);
|
||||
|
||||
|
||||
// Low acceptance should decrease step size
|
||||
let current_step = proposal.step_size;
|
||||
proposal.adapt(0.1);
|
||||
|
||||
@@ -17,7 +17,7 @@ pub fn mcmc_sample(
|
||||
burn_in: Option<usize>,
|
||||
) -> PyResult<Vec<Vec<f64>>> {
|
||||
let burn_in = burn_in.unwrap_or(n_samples / 10);
|
||||
|
||||
|
||||
let proposal = GaussianProposal::new(step_size);
|
||||
let config = MCMCConfig {
|
||||
n_samples,
|
||||
@@ -27,10 +27,10 @@ pub fn mcmc_sample(
|
||||
proposal,
|
||||
adaptation_interval: 100,
|
||||
};
|
||||
|
||||
|
||||
let log_likelihood = PyLogLikelihood::new(log_likelihood_fn);
|
||||
let mut sampler = MetropolisHastings::new(config, log_likelihood);
|
||||
|
||||
|
||||
sampler
|
||||
.sample_chain()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
|
||||
@@ -47,7 +47,7 @@ pub fn adaptive_mcmc_sample(
|
||||
burn_in: Option<usize>,
|
||||
) -> PyResult<Vec<Vec<f64>>> {
|
||||
let burn_in = burn_in.unwrap_or(n_samples / 10);
|
||||
|
||||
|
||||
let proposal = AdaptiveProposal::new(initial_step);
|
||||
let config = MCMCConfig {
|
||||
n_samples,
|
||||
@@ -57,10 +57,10 @@ pub fn adaptive_mcmc_sample(
|
||||
proposal,
|
||||
adaptation_interval: 100,
|
||||
};
|
||||
|
||||
|
||||
let log_likelihood = PyLogLikelihood::new(log_likelihood_fn);
|
||||
let mut sampler = MetropolisHastings::new(config, log_likelihood);
|
||||
|
||||
|
||||
sampler
|
||||
.sample_chain()
|
||||
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
|
||||
|
||||
+25
-30
@@ -21,32 +21,32 @@ impl<P: ProposalStrategy, L: LogLikelihood> MetropolisHastings<P, L> {
|
||||
log_likelihood,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Run MCMC chain
|
||||
pub fn sample_chain(&mut self) -> Result<Vec<Vec<f64>>> {
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut current_state = self.config.initial_state.clone();
|
||||
let mut current_ll = self.log_likelihood.evaluate(¤t_state);
|
||||
|
||||
|
||||
let total_steps = self.config.n_samples + self.config.burn_in;
|
||||
let mut samples = Vec::with_capacity(self.config.n_samples / self.config.thin);
|
||||
let mut acceptance_count = 0usize;
|
||||
|
||||
|
||||
for step in 0..total_steps {
|
||||
// Propose new state
|
||||
let proposed_state = self.config.proposal.propose(¤t_state, &mut rng);
|
||||
let proposed_ll = self.log_likelihood.evaluate(&proposed_state);
|
||||
|
||||
|
||||
// Metropolis-Hastings acceptance
|
||||
let log_alpha = proposed_ll - current_ll;
|
||||
let accepted = log_alpha >= 0.0 || rng.gen::<f64>() < log_alpha.exp();
|
||||
|
||||
|
||||
if accepted {
|
||||
current_state = proposed_state;
|
||||
current_ll = proposed_ll;
|
||||
acceptance_count += 1;
|
||||
}
|
||||
|
||||
|
||||
// Adapt proposal if needed
|
||||
if step > 0 && step % self.config.adaptation_interval == 0 {
|
||||
let acceptance_rate =
|
||||
@@ -54,65 +54,60 @@ impl<P: ProposalStrategy, L: LogLikelihood> MetropolisHastings<P, L> {
|
||||
self.config.proposal.adapt(acceptance_rate);
|
||||
acceptance_count = 0;
|
||||
}
|
||||
|
||||
|
||||
// Store sample after burn-in
|
||||
if step >= self.config.burn_in && (step - self.config.burn_in) % self.config.thin == 0
|
||||
{
|
||||
if step >= self.config.burn_in && (step - self.config.burn_in) % self.config.thin == 0 {
|
||||
samples.push(current_state.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(samples)
|
||||
}
|
||||
|
||||
|
||||
/// Compute diagnostics for chain
|
||||
pub fn diagnostics(&self, samples: &[Vec<f64>]) -> Result<SamplerDiagnostics> {
|
||||
if samples.is_empty() {
|
||||
return Err(OptimizrError::EmptyData);
|
||||
}
|
||||
|
||||
|
||||
let n_samples = samples.len();
|
||||
let dim = samples[0].len();
|
||||
|
||||
|
||||
// Compute means and variances
|
||||
let means: Vec<f64> = (0..dim)
|
||||
.map(|d| samples.iter().map(|s| s[d]).sum::<f64>() / n_samples as f64)
|
||||
.collect();
|
||||
|
||||
|
||||
let variances: Vec<f64> = (0..dim)
|
||||
.map(|d| {
|
||||
let mean = means[d];
|
||||
samples
|
||||
.iter()
|
||||
.map(|s| (s[d] - mean).powi(2))
|
||||
.sum::<f64>()
|
||||
/ (n_samples - 1) as f64
|
||||
samples.iter().map(|s| (s[d] - mean).powi(2)).sum::<f64>() / (n_samples - 1) as f64
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
// Compute autocorrelations (lag 1)
|
||||
let autocorrs: Vec<f64> = (0..dim)
|
||||
.map(|d| {
|
||||
if n_samples < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
|
||||
let mean = means[d];
|
||||
let var = variances[d];
|
||||
|
||||
|
||||
if var < 1e-10 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
|
||||
let cov: f64 = (0..n_samples - 1)
|
||||
.map(|i| (samples[i][d] - mean) * (samples[i + 1][d] - mean))
|
||||
.sum::<f64>()
|
||||
/ (n_samples - 1) as f64;
|
||||
|
||||
|
||||
cov / var
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
Ok(SamplerDiagnostics {
|
||||
n_samples,
|
||||
means,
|
||||
@@ -127,11 +122,11 @@ impl<P: ProposalStrategy + 'static, L: LogLikelihood + 'static> Sampler
|
||||
{
|
||||
type Config = MCMCConfig<P>;
|
||||
type Output = Vec<Vec<f64>>;
|
||||
|
||||
|
||||
fn sample(&mut self) -> Result<Self::Output> {
|
||||
self.sample_chain()
|
||||
}
|
||||
|
||||
|
||||
fn diagnostics(&self, samples: &Self::Output) -> Result<SamplerDiagnostics> {
|
||||
self.diagnostics(samples)
|
||||
}
|
||||
@@ -143,7 +138,7 @@ mod tests {
|
||||
use crate::mcmc::proposal::GaussianProposal;
|
||||
|
||||
struct TestLogLikelihood;
|
||||
|
||||
|
||||
impl LogLikelihood for TestLogLikelihood {
|
||||
fn evaluate(&self, state: &[f64]) -> f64 {
|
||||
-0.5 * state.iter().map(|x| x.powi(2)).sum::<f64>()
|
||||
@@ -160,10 +155,10 @@ mod tests {
|
||||
proposal: GaussianProposal::new(0.5),
|
||||
adaptation_interval: 50,
|
||||
};
|
||||
|
||||
|
||||
let log_likelihood = TestLogLikelihood;
|
||||
let mut sampler = MetropolisHastings::new(config, log_likelihood);
|
||||
|
||||
|
||||
let samples = sampler.sample_chain().unwrap();
|
||||
assert_eq!(samples.len(), 100);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user