use super::*;
#[cfg_attr(feature = "python_stubgen", gen_stub_pyclass)]
#[pyclass(from_py_object, name = "Sample", module = "symbolica.core")]
#[derive(Clone)]
pub struct PythonSample {
#[pyo3(get)]
weights: Vec<f64>,
#[pyo3(get)]
d: Vec<usize>,
#[pyo3(get)]
c: Vec<f64>,
uniform: bool,
}
impl PythonSample {
fn into_sample(self) -> Sample<f64> {
if self.uniform {
return Sample::Uniform(self.weights[0], self.d, self.c);
}
assert_eq!(
self.weights.len(),
self.d.len() + if self.c.is_empty() { 0 } else { 1 }
);
let mut weight_index = self.weights.len() - 1;
let mut sample = if !self.c.is_empty() {
Some(Sample::Continuous(self.weights[weight_index], self.c))
} else {
None
};
for dd in self.d.iter().rev() {
weight_index -= 1;
sample = Some(Sample::Discrete(
self.weights[weight_index],
*dd,
sample.map(Box::new),
));
}
sample.unwrap()
}
fn from_sample(mut sample: &Sample<f64>) -> PythonSample {
let mut weights = vec![];
let mut d = vec![];
let mut c = vec![];
let mut uniform = false;
loop {
match sample {
Sample::Continuous(w, cs) => {
weights.push(*w);
c.extend_from_slice(cs);
break;
}
Sample::Discrete(w, i, s) => {
weights.push(*w);
d.push(*i);
if let Some(ss) = s {
sample = ss;
} else {
break;
}
}
Sample::Uniform(w, i, cs) => {
weights.push(*w);
d.clone_from(i);
c.clone_from(cs);
uniform = true;
break;
}
}
}
PythonSample {
weights,
d,
c,
uniform,
}
}
}
#[cfg_attr(feature = "python_stubgen", gen_stub_pyclass)]
#[pyclass(from_py_object, name = "Probe", module = "symbolica.core")]
#[derive(Clone)]
pub struct PythonProbe {
#[pyo3(get)]
d: Vec<usize>,
#[pyo3(get)]
c: Vec<Option<f64>>,
u: Vec<Option<usize>>,
}
impl PythonProbe {
pub fn into_probe(self) -> Probe<f64> {
if self.u.is_empty() {
if self.d.is_empty() {
Probe::Continuous(self.c)
} else {
Probe::Discrete(self.d, self.c)
}
} else {
Probe::Uniform(self.u, self.c)
}
}
}
#[cfg_attr(feature = "python_stubgen", gen_stub_pymethods)]
#[cfg_attr(not(feature = "python_stubgen"), remove_gen_stub)]
#[pymethods]
impl PythonProbe {
#[pyo3(signature = (disc, cont=None))]
#[classmethod]
pub fn discrete(
_cls: &Bound<'_, PyType>,
disc: Vec<usize>,
cont: Option<Vec<Option<f64>>>,
) -> Self {
Self {
d: disc,
c: cont.unwrap_or_default(),
u: Vec::new(),
}
}
#[classmethod]
pub fn continuous(_cls: &Bound<'_, PyType>, cont: Vec<Option<f64>>) -> Self {
Self {
d: Vec::new(),
c: cont,
u: Vec::new(),
}
}
#[pyo3(signature = (uni, cont=None))]
#[classmethod]
pub fn uniform(
_cls: &Bound<'_, PyType>,
uni: Vec<Option<usize>>,
cont: Option<Vec<Option<f64>>>,
) -> Self {
Self {
d: Vec::new(),
c: cont.unwrap_or_default(),
u: uni,
}
}
}
#[cfg_attr(feature = "python_stubgen", gen_stub_pyclass)]
#[pyclass(
from_py_object,
name = "RandomNumberGenerator",
module = "symbolica.core"
)]
#[derive(Clone)]
pub struct PythonRandomNumberGenerator {
state: MonteCarloRng,
}
#[cfg_attr(feature = "python_stubgen", gen_stub_pymethods)]
#[cfg_attr(not(feature = "python_stubgen"), remove_gen_stub)]
#[pymethods]
impl PythonRandomNumberGenerator {
#[new]
fn new(seed: u64, stream_id: usize) -> Self {
Self {
state: MonteCarloRng::new(seed, stream_id),
}
}
fn __copy__(&self) -> Self {
self.clone()
}
fn next(&mut self) -> u64 {
self.state.next_u64()
}
fn next_float(&mut self) -> f64 {
self.state.random()
}
#[classmethod]
fn load(_cls: &Bound<'_, PyType>, state: Bound<'_, PyBytes>) -> PyResult<Self> {
let state: [u8; 32] = state.as_bytes().try_into().map_err(|_| {
exceptions::PyValueError::new_err("Invalid state size: expected 32 bytes")
})?;
Ok(PythonRandomNumberGenerator {
state: MonteCarloRng::import(state),
})
}
fn save<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
let state = self.state.export();
PyBytes::new(py, &state).into()
}
}
#[cfg_attr(feature = "python_stubgen", gen_stub_pyclass)]
#[pyclass(
from_py_object,
name = "NumericalIntegrator",
module = "symbolica.core"
)]
#[derive(Clone)]
pub struct PythonNumericalIntegrator {
grid: Grid<f64>,
}
#[cfg(feature = "python_stubgen")]
impl_stub_type!(&mut PythonRandomNumberGenerator = PythonRandomNumberGenerator);
#[cfg_attr(feature = "python_stubgen", gen_stub_pymethods)]
#[cfg_attr(not(feature = "python_stubgen"), remove_gen_stub)]
#[pymethods]
impl PythonNumericalIntegrator {
#[classmethod]
#[pyo3(signature =
(n_dims, n_bins = 128,
min_samples_for_update = 100,
bin_number_evolution = None,
train_on_avg = false,
min_probability_density = 0.)
)]
pub fn continuous(
_cls: &Bound<'_, PyType>,
n_dims: usize,
n_bins: usize,
min_samples_for_update: usize,
bin_number_evolution: Option<Vec<usize>>,
train_on_avg: bool,
min_probability_density: f64,
) -> PyResult<PythonNumericalIntegrator> {
Ok(PythonNumericalIntegrator {
grid: Grid::Continuous(
ContinuousGrid::new_with_min_probability_density(
n_dims,
n_bins,
min_samples_for_update,
bin_number_evolution,
train_on_avg,
min_probability_density,
)
.map_err(exceptions::PyValueError::new_err)?,
),
})
}
#[classmethod]
#[pyo3(signature =
(bins,
max_prob_ratio = 100.,
train_on_avg = false)
)]
pub fn discrete(
_cls: &Bound<'_, PyType>,
bins: Vec<Option<PythonNumericalIntegrator>>,
max_prob_ratio: f64,
train_on_avg: bool,
) -> PythonNumericalIntegrator {
let bins = bins.into_iter().map(|b| b.map(|bb| bb.grid)).collect();
PythonNumericalIntegrator {
grid: Grid::Discrete(DiscreteGrid::new(bins, max_prob_ratio, train_on_avg)),
}
}
#[classmethod]
pub fn uniform(
_cls: &Bound<'_, PyType>,
bins: Vec<usize>,
continuous_grid: PythonNumericalIntegrator,
) -> PyResult<PythonNumericalIntegrator> {
if let Grid::Continuous(g) = continuous_grid.grid {
Ok(PythonNumericalIntegrator {
grid: Grid::Uniform(bins, g),
})
} else {
return PyResult::Err(pyo3::exceptions::PyAssertionError::new_err(
"The specified grid is not a continuous grid",
));
}
}
#[classmethod]
pub fn rng(
_cls: &Bound<'_, PyType>,
seed: u64,
stream_id: usize,
) -> PythonRandomNumberGenerator {
PythonRandomNumberGenerator::new(seed, stream_id)
}
pub fn __copy__(&self) -> Self {
Self {
grid: self.grid.clone_without_samples(),
}
}
pub fn probe(&self, probe: PythonProbe) -> PyResult<f64> {
self.grid
.probe(&probe.into_probe())
.map_err(|e| exceptions::PyValueError::new_err(e.to_string()))
}
pub fn sample(
&mut self,
num_samples: usize,
rng: &mut PythonRandomNumberGenerator,
) -> Vec<PythonSample> {
let mut sample = Sample::new();
let mut samples = Vec::with_capacity(num_samples);
for _ in 0..num_samples {
self.grid.sample(&mut rng.state, &mut sample);
samples.push(PythonSample::from_sample(&sample));
}
samples
}
fn add_training_samples(
&mut self,
samples: Vec<PythonSample>,
evals: Vec<f64>,
) -> PyResult<()> {
if evals.len() != samples.len() {
return PyResult::Err(pyo3::exceptions::PyAssertionError::new_err(
"Number of returned values does not equal number of samples",
));
}
for (s, f) in samples.into_iter().zip(evals) {
self.grid
.add_training_sample(&s.into_sample(), f)
.map_err(pyo3::exceptions::PyAssertionError::new_err)?;
}
Ok(())
}
#[classmethod]
fn import_grid(_cls: &Bound<'_, PyType>, grid: Bound<'_, PyBytes>) -> PyResult<Self> {
let grid = bincode::decode_from_slice(grid.extract()?, bincode::config::standard())
.map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string()))?
.0;
Ok(PythonNumericalIntegrator { grid })
}
#[pyo3(signature = (export_samples = true))]
fn export_grid<'p>(
&self,
export_samples: bool,
py: Python<'p>,
) -> PyResult<Bound<'p, PyBytes>> {
if export_samples {
bincode::encode_to_vec(&self.grid, bincode::config::standard())
} else {
bincode::encode_to_vec(
&self.grid.clone_without_samples(),
bincode::config::standard(),
)
}
.map(|a| PyBytes::new(py, &a))
.map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string()))
}
fn get_live_estimate(&self) -> PyResult<(f64, f64, f64, f64, f64, usize)> {
match &self.grid {
Grid::Continuous(cs) | Grid::Uniform(_, cs) => {
let mut a = cs.accumulator.shallow_copy();
a.update_iter(false);
Ok((
a.avg,
a.err,
a.chi_sq,
a.max_eval_negative,
a.max_eval_positive,
a.processed_samples,
))
}
Grid::Discrete(ds) => {
let mut a = ds.accumulator.shallow_copy();
a.update_iter(false);
Ok((
a.avg,
a.err,
a.chi_sq,
a.max_eval_negative,
a.max_eval_positive,
a.processed_samples,
))
}
}
}
fn merge(&mut self, other: &PythonNumericalIntegrator) -> PyResult<()> {
self.grid
.merge(&other.grid)
.map_err(pyo3::exceptions::PyAssertionError::new_err)
}
fn update(
&mut self,
discrete_learning_rate: f64,
continuous_learning_rate: f64,
) -> PyResult<(f64, f64, f64)> {
self.grid
.update(discrete_learning_rate, continuous_learning_rate);
let stats = self.grid.get_statistics();
Ok((stats.avg, stats.err, stats.chi_sq / stats.cur_iter as f64))
}
#[pyo3(signature =
(integrand,
max_n_iter = 10_000_000,
min_error = 0.01,
n_samples_per_iter = 10_000,
seed = 0,
show_stats = true)
)]
pub fn integrate(
&mut self,
py: Python,
#[gen_stub(override_type(
type_repr = "typing.Callable[[typing.Sequence[Sample]], list[float]]"
))]
integrand: Py<PyAny>,
max_n_iter: usize,
min_error: f64,
n_samples_per_iter: usize,
seed: u64,
show_stats: bool,
) -> PyResult<(f64, f64, f64)> {
let mut rng = MonteCarloRng::new(seed, 0);
let mut samples = vec![Sample::new(); n_samples_per_iter];
for iteration in 1..=max_n_iter {
for sample in &mut samples {
self.grid.sample(&mut rng, sample);
}
let p_samples: Vec<_> = samples.iter().map(PythonSample::from_sample).collect();
let res = integrand
.call(py, (p_samples,), None)?
.extract::<Vec<f64>>(py)?;
if res.len() != n_samples_per_iter {
return Err(exceptions::PyValueError::new_err(
"Wrong number of arguments returned for integration function.",
));
}
for (s, r) in samples.iter().zip(res) {
self.grid.add_training_sample(s, r).unwrap();
}
self.grid.update(1.5, 1.5);
let stats = self.grid.get_statistics();
if show_stats {
println!(
"Iteration {:2}: {} {:.2} χ²",
iteration,
stats.format_uncertainty(),
stats.chi_sq / stats.cur_iter as f64
);
}
if stats.avg != 0. && stats.err / stats.avg.abs() <= min_error {
break;
}
}
let stats = self.grid.get_statistics();
Ok((stats.avg, stats.err, stats.chi_sq / stats.cur_iter as f64))
}
}