use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use crate::anomaly::{EwmaDetector, MadDetector, ZScoreDetector};
use crate::privacy::{LaplaceNoise, RandomizedResponse};
use crate::sketch::{CountMinSketch2048x7, DDSketch256, FnvHasher, HyperLogLog14, Mergeable};
#[pyclass(name = "HyperLogLog")]
pub struct PyHyperLogLog {
inner: HyperLogLog14,
}
#[pymethods]
impl PyHyperLogLog {
#[new]
fn new() -> Self {
Self {
inner: HyperLogLog14::new(),
}
}
fn insert_hash(&mut self, hash: u64) {
self.inner.insert_hash(hash);
}
fn insert_str(&mut self, value: &str) {
self.inner
.insert_hash(FnvHasher::hash_bytes(value.as_bytes()));
}
fn insert_batch(&mut self, hashes: PyReadonlyArray1<'_, u64>) -> PyResult<()> {
let slice = hashes
.as_slice()
.map_err(|e| PyValueError::new_err(e.to_string()))?;
for &h in slice {
self.inner.insert_hash(h);
}
Ok(())
}
fn cardinality(&self) -> f64 {
self.inner.cardinality()
}
fn merge(&mut self, other: &PyHyperLogLog) {
self.inner.merge(&other.inner);
}
fn clear(&mut self) {
self.inner.clear();
}
fn __repr__(&self) -> String {
format!("HyperLogLog(cardinality={:.0})", self.inner.cardinality())
}
}
#[pyclass(name = "DDSketch")]
pub struct PyDDSketch {
inner: DDSketch256,
}
#[pymethods]
impl PyDDSketch {
#[new]
#[pyo3(signature = (alpha=0.02))]
fn new(alpha: f64) -> Self {
Self {
inner: DDSketch256::new(alpha),
}
}
fn insert(&mut self, value: f64) {
self.inner.insert(value);
}
fn insert_batch(&mut self, values: PyReadonlyArray1<'_, f64>) -> PyResult<()> {
let slice = values
.as_slice()
.map_err(|e| PyValueError::new_err(e.to_string()))?;
for &v in slice {
self.inner.insert(v);
}
Ok(())
}
fn quantile(&self, q: f64) -> f64 {
self.inner.quantile(q)
}
fn count(&self) -> u64 {
self.inner.count()
}
fn sum(&self) -> f64 {
self.inner.sum()
}
fn mean(&self) -> f64 {
self.inner.mean()
}
fn min(&self) -> f64 {
self.inner.min()
}
fn max(&self) -> f64 {
self.inner.max()
}
fn merge(&mut self, other: &PyDDSketch) {
self.inner.merge(&other.inner);
}
fn clear(&mut self) {
self.inner.clear();
}
fn __repr__(&self) -> String {
format!(
"DDSketch(count={}, p50={:.2}, p99={:.2})",
self.inner.count(),
self.inner.quantile(0.50),
self.inner.quantile(0.99)
)
}
}
#[pyclass(name = "CountMinSketch")]
pub struct PyCountMinSketch {
inner: CountMinSketch2048x7,
}
#[pymethods]
impl PyCountMinSketch {
#[new]
fn new() -> Self {
Self {
inner: CountMinSketch2048x7::new(),
}
}
fn insert_hash(&mut self, hash: u64) {
self.inner.insert_hash(hash, 1);
}
fn insert_str(&mut self, value: &str) {
self.inner
.insert_hash(FnvHasher::hash_bytes(value.as_bytes()), 1);
}
fn insert_batch(&mut self, hashes: PyReadonlyArray1<'_, u64>) -> PyResult<()> {
let slice = hashes
.as_slice()
.map_err(|e| PyValueError::new_err(e.to_string()))?;
for &h in slice {
self.inner.insert_hash(h, 1);
}
Ok(())
}
fn estimate_hash(&self, hash: u64) -> u64 {
self.inner.estimate_hash(hash)
}
fn estimate_str(&self, value: &str) -> u64 {
self.inner
.estimate_hash(FnvHasher::hash_bytes(value.as_bytes()))
}
fn total(&self) -> u64 {
self.inner.total()
}
fn merge(&mut self, other: &PyCountMinSketch) {
self.inner.merge(&other.inner);
}
fn clear(&mut self) {
self.inner.clear();
}
fn __repr__(&self) -> String {
format!("CountMinSketch(total={})", self.inner.total())
}
}
#[pyclass(name = "MadDetector")]
pub struct PyMadDetector {
inner: MadDetector,
}
#[pymethods]
impl PyMadDetector {
#[new]
#[pyo3(signature = (threshold_k=3.0))]
fn new(threshold_k: f64) -> Self {
Self {
inner: MadDetector::new(threshold_k),
}
}
fn observe(&mut self, value: f64) {
self.inner.observe(value);
}
fn observe_batch(&mut self, values: PyReadonlyArray1<'_, f64>) -> PyResult<()> {
let slice = values
.as_slice()
.map_err(|e| PyValueError::new_err(e.to_string()))?;
for &v in slice {
self.inner.observe(v);
}
Ok(())
}
fn is_anomaly(&mut self, value: f64) -> bool {
self.inner.is_anomaly(value)
}
fn anomaly_score(&mut self, value: f64) -> f64 {
self.inner.anomaly_score(value)
}
fn is_anomaly_batch<'py>(
&mut self,
py: Python<'py>,
values: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<bool>>> {
let slice = values
.as_slice()
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let result: Vec<bool> = slice.iter().map(|&v| self.inner.is_anomaly(v)).collect();
Ok(result.into_pyarray(py))
}
fn anomaly_score_batch<'py>(
&mut self,
py: Python<'py>,
values: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let slice = values
.as_slice()
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let result: Vec<f64> = slice.iter().map(|&v| self.inner.anomaly_score(v)).collect();
Ok(result.into_pyarray(py))
}
fn median(&mut self) -> f64 {
self.inner.median()
}
fn mad(&mut self) -> f64 {
self.inner.mad()
}
fn count(&self) -> usize {
self.inner.count()
}
fn clear(&mut self) {
self.inner.clear();
}
}
#[pyclass(name = "EwmaDetector")]
pub struct PyEwmaDetector {
inner: EwmaDetector,
}
#[pymethods]
impl PyEwmaDetector {
#[new]
#[pyo3(signature = (alpha=0.1, threshold_k=3.0))]
fn new(alpha: f64, threshold_k: f64) -> Self {
Self {
inner: EwmaDetector::new(alpha, threshold_k),
}
}
fn observe(&mut self, value: f64) {
self.inner.observe(value);
}
fn observe_batch(&mut self, values: PyReadonlyArray1<'_, f64>) -> PyResult<()> {
let slice = values
.as_slice()
.map_err(|e| PyValueError::new_err(e.to_string()))?;
for &v in slice {
self.inner.observe(v);
}
Ok(())
}
fn is_anomaly(&self, value: f64) -> bool {
self.inner.is_anomaly(value)
}
fn anomaly_score(&self, value: f64) -> f64 {
self.inner.anomaly_score(value)
}
fn is_anomaly_batch<'py>(
&self,
py: Python<'py>,
values: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<bool>>> {
let slice = values
.as_slice()
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let detector = &self.inner;
let result = py.detach(|| {
slice
.iter()
.map(|&v| detector.is_anomaly(v))
.collect::<Vec<bool>>()
});
Ok(result.into_pyarray(py))
}
fn anomaly_score_batch<'py>(
&self,
py: Python<'py>,
values: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let slice = values
.as_slice()
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let detector = &self.inner;
let result = py.detach(|| {
slice
.iter()
.map(|&v| detector.anomaly_score(v))
.collect::<Vec<f64>>()
});
Ok(result.into_pyarray(py))
}
fn ewma(&self) -> f64 {
self.inner.ewma()
}
fn std_dev(&self) -> f64 {
self.inner.std_dev()
}
fn count(&self) -> u64 {
self.inner.count()
}
fn reset(&mut self) {
self.inner.reset();
}
}
#[pyclass(name = "ZScoreDetector")]
pub struct PyZScoreDetector {
inner: ZScoreDetector,
}
#[pymethods]
impl PyZScoreDetector {
#[new]
#[pyo3(signature = (threshold_k=3.0))]
fn new(threshold_k: f64) -> Self {
Self {
inner: ZScoreDetector::new(threshold_k),
}
}
fn observe(&mut self, value: f64) {
self.inner.observe(value);
}
fn observe_batch(&mut self, values: PyReadonlyArray1<'_, f64>) -> PyResult<()> {
let slice = values
.as_slice()
.map_err(|e| PyValueError::new_err(e.to_string()))?;
for &v in slice {
self.inner.observe(v);
}
Ok(())
}
fn is_anomaly(&self, value: f64) -> bool {
self.inner.is_anomaly(value)
}
fn anomaly_score(&self, value: f64) -> f64 {
self.inner.z_score(value)
}
fn is_anomaly_batch<'py>(
&self,
py: Python<'py>,
values: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<bool>>> {
let slice = values
.as_slice()
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let detector = &self.inner;
let result = py.detach(|| {
slice
.iter()
.map(|&v| detector.is_anomaly(v))
.collect::<Vec<bool>>()
});
Ok(result.into_pyarray(py))
}
fn anomaly_score_batch<'py>(
&self,
py: Python<'py>,
values: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let slice = values
.as_slice()
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let detector = &self.inner;
let result = py.detach(|| {
slice
.iter()
.map(|&v| detector.z_score(v))
.collect::<Vec<f64>>()
});
Ok(result.into_pyarray(py))
}
fn count(&self) -> u64 {
self.inner.count()
}
}
#[pyclass(name = "LaplaceNoise")]
pub struct PyLaplaceNoise {
inner: LaplaceNoise,
}
#[pymethods]
impl PyLaplaceNoise {
#[new]
fn new(sensitivity: f64, epsilon: f64) -> Self {
Self {
inner: LaplaceNoise::new(sensitivity, epsilon),
}
}
#[staticmethod]
fn with_seed(sensitivity: f64, epsilon: f64, seed: u64) -> Self {
Self {
inner: LaplaceNoise::with_seed(sensitivity, epsilon, seed),
}
}
fn privatize(&mut self, value: f64) -> f64 {
self.inner.privatize(value)
}
fn privatize_batch<'py>(
&mut self,
py: Python<'py>,
values: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let slice = values
.as_slice()
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let result: Vec<f64> = slice.iter().map(|&v| self.inner.privatize(v)).collect();
Ok(result.into_pyarray(py))
}
fn scale(&self) -> f64 {
self.inner.scale()
}
}
#[pyclass(name = "RandomizedResponse")]
pub struct PyRandomizedResponse {
inner: RandomizedResponse,
}
#[pymethods]
impl PyRandomizedResponse {
#[new]
fn new(epsilon: f64) -> Self {
Self {
inner: RandomizedResponse::new(epsilon),
}
}
fn privatize(&mut self, value: bool) -> bool {
self.inner.privatize(value)
}
fn p_true(&self) -> f64 {
self.inner.p_true()
}
#[staticmethod]
fn estimate_proportion(p_true: f64, n: u64, k: u64) -> f64 {
RandomizedResponse::estimate_proportion(p_true, n, k)
}
}
#[pyfunction]
fn fnv_hash_bytes(data: &[u8]) -> u64 {
FnvHasher::hash_bytes(data)
}
#[pyfunction]
fn fnv_hash_u64(value: u64) -> u64 {
FnvHasher::hash_u64(value)
}
#[pyfunction]
fn fnv_hash_batch<'py>(
py: Python<'py>,
values: PyReadonlyArray1<'py, u64>,
) -> PyResult<Bound<'py, PyArray1<u64>>> {
let slice = values
.as_slice()
.map_err(|e| PyValueError::new_err(e.to_string()))?;
let result = py.detach(|| {
slice
.iter()
.map(|&v| FnvHasher::hash_u64(v))
.collect::<Vec<u64>>()
});
Ok(result.into_pyarray(py))
}
#[pymodule]
pub fn alice_analytics(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyHyperLogLog>()?;
m.add_class::<PyDDSketch>()?;
m.add_class::<PyCountMinSketch>()?;
m.add_class::<PyMadDetector>()?;
m.add_class::<PyEwmaDetector>()?;
m.add_class::<PyZScoreDetector>()?;
m.add_class::<PyLaplaceNoise>()?;
m.add_class::<PyRandomizedResponse>()?;
m.add_function(wrap_pyfunction!(fnv_hash_bytes, m)?)?;
m.add_function(wrap_pyfunction!(fnv_hash_u64, m)?)?;
m.add_function(wrap_pyfunction!(fnv_hash_batch, m)?)?;
Ok(())
}