use crate::algebra::TriplePattern;
use crate::statistics::PatternStatistics;
use scirs2_core::ndarray_ext::Array1;
use scirs2_core::random::Random;
use scirs2_core::rngs::StdRng;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum EstimationError {
#[error("No statistics available for predicate: {0}")]
NoStatistics(String),
#[error("Invalid histogram configuration: {0}")]
InvalidHistogram(String),
#[error("Sampling error: {0}")]
SamplingError(String),
}
pub type Result<T> = std::result::Result<T, EstimationError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EstimationMethod {
Simple,
Histogram,
Sampling,
Sketch,
MachineLearning,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistogramBucket {
pub lower_bound: f64,
pub upper_bound: f64,
pub count: u64,
pub distinct_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PredicateHistogram {
pub predicate: String,
pub subject_buckets: Vec<HistogramBucket>,
pub object_buckets: Vec<HistogramBucket>,
pub total_count: u64,
pub distinct_subjects: u64,
pub distinct_objects: u64,
}
impl PredicateHistogram {
pub fn new(predicate: String, num_buckets: usize) -> Self {
Self {
predicate,
subject_buckets: Vec::with_capacity(num_buckets),
object_buckets: Vec::with_capacity(num_buckets),
total_count: 0,
distinct_subjects: 0,
distinct_objects: 0,
}
}
pub fn estimate_selectivity(&self, lower: f64, upper: f64, is_subject: bool) -> f64 {
let buckets = if is_subject {
&self.subject_buckets
} else {
&self.object_buckets
};
if buckets.is_empty() {
return 1.0; }
let mut matching_count = 0u64;
for bucket in buckets {
if bucket.upper_bound >= lower && bucket.lower_bound <= upper {
if bucket.lower_bound >= lower && bucket.upper_bound <= upper {
matching_count += bucket.count;
} else {
let overlap = (bucket.upper_bound.min(upper) - bucket.lower_bound.max(lower))
/ (bucket.upper_bound - bucket.lower_bound);
matching_count += (bucket.count as f64 * overlap) as u64;
}
}
}
if self.total_count == 0 {
return 0.0;
}
matching_count as f64 / self.total_count as f64
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HyperLogLogSketch {
num_registers: usize,
registers: Vec<u8>,
precision: u8,
}
impl HyperLogLogSketch {
pub fn new(precision: u8) -> Self {
let num_registers = 1 << precision;
Self {
num_registers,
registers: vec![0; num_registers],
precision,
}
}
pub fn add(&mut self, element: &str) {
let hash = self.hash_element(element);
let register_idx = (hash & ((1 << self.precision) - 1)) as usize;
let remaining = hash >> self.precision;
let leading_zeros = if remaining == 0 {
64 - self.precision
} else {
remaining.leading_zeros() as u8 + 1
};
self.registers[register_idx] = self.registers[register_idx].max(leading_zeros);
}
pub fn estimate_cardinality(&self) -> u64 {
let alpha = match self.num_registers {
16 => 0.673,
32 => 0.697,
64 => 0.709,
_ => 0.7213 / (1.0 + 1.079 / self.num_registers as f64),
};
let raw_estimate = alpha
* (self.num_registers as f64).powi(2)
* (1.0
/ self
.registers
.iter()
.map(|&v| 2.0f64.powi(-(v as i32)))
.sum::<f64>());
if raw_estimate <= 2.5 * self.num_registers as f64 {
let zero_registers = self.registers.iter().filter(|&&v| v == 0).count();
if zero_registers > 0 {
return (self.num_registers as f64
* (self.num_registers as f64 / zero_registers as f64).ln())
as u64;
}
} else if raw_estimate > (1u64 << 32) as f64 / 30.0 {
return (-((1u64 << 32) as f64) * (1.0 - raw_estimate / (1u64 << 32) as f64).ln())
as u64;
}
raw_estimate as u64
}
fn hash_element(&self, element: &str) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
element.hash(&mut hasher);
hasher.finish()
}
pub fn merge(&mut self, other: &HyperLogLogSketch) {
if self.precision != other.precision {
return; }
for (i, &other_val) in other.registers.iter().enumerate() {
self.registers[i] = self.registers[i].max(other_val);
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ReservoirSample {
sample_size: usize,
samples: Vec<String>,
elements_seen: u64,
#[serde(skip)]
rng: Option<Random<StdRng>>,
}
impl Clone for ReservoirSample {
fn clone(&self) -> Self {
Self {
sample_size: self.sample_size,
samples: self.samples.clone(),
elements_seen: self.elements_seen,
rng: None,
}
}
}
impl ReservoirSample {
pub fn new(sample_size: usize) -> Self {
Self {
sample_size,
samples: Vec::with_capacity(sample_size),
elements_seen: 0,
rng: Some(Random::seed(42)),
}
}
pub fn add(&mut self, element: String) {
self.elements_seen += 1;
if self.samples.len() < self.sample_size {
self.samples.push(element);
} else {
let rng = self.rng.get_or_insert_with(|| Random::seed(42));
let idx = rng.random_range(0..self.elements_seen);
if (idx as usize) < self.sample_size {
self.samples[idx as usize] = element;
}
}
}
pub fn estimate_distinct(&self) -> u64 {
if self.elements_seen == 0 {
return 0;
}
let mut unique = std::collections::HashSet::new();
for elem in &self.samples {
unique.insert(elem);
}
let sample_distinct = unique.len() as f64;
let sample_size = self.samples.len() as f64;
let estimated_distinct = sample_distinct * self.elements_seen as f64 / sample_size;
estimated_distinct.min(self.elements_seen as f64) as u64
}
pub fn coverage(&self) -> f64 {
if self.elements_seen == 0 {
return 0.0;
}
self.samples.len() as f64 / self.elements_seen as f64
}
}
#[derive(Debug, Clone, Default)]
struct CardinalityCache {
max_size: usize,
cache: HashMap<String, (u64, usize)>,
access_counter: usize,
lru_queue: VecDeque<String>,
}
impl CardinalityCache {
fn new(max_size: usize) -> Self {
if max_size == 0 {
Self::default()
} else {
Self {
max_size,
cache: HashMap::with_capacity(max_size),
access_counter: 0,
lru_queue: VecDeque::with_capacity(max_size),
}
}
}
fn get(&mut self, key: &str) -> Option<u64> {
if let Some((cardinality, _)) = self.cache.get_mut(key) {
self.access_counter += 1;
let card = *cardinality;
self.cache
.insert(key.to_string(), (card, self.access_counter));
Some(card)
} else {
None
}
}
fn insert(&mut self, key: String, cardinality: u64) {
if self.cache.len() >= self.max_size && !self.cache.contains_key(&key) {
if let Some(lru_key) = self.lru_queue.pop_front() {
self.cache.remove(&lru_key);
}
}
self.access_counter += 1;
self.cache
.insert(key.clone(), (cardinality, self.access_counter));
self.lru_queue.push_back(key);
}
fn clear(&mut self) {
self.cache.clear();
self.lru_queue.clear();
self.access_counter = 0;
}
fn len(&self) -> usize {
self.cache.len()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CardinalityEstimator {
method: EstimationMethod,
pattern_stats: HashMap<String, PatternStatistics>,
histograms: HashMap<String, PredicateHistogram>,
#[serde(skip)]
sketches: HashMap<String, HyperLogLogSketch>,
#[serde(skip)]
samples: HashMap<String, ReservoirSample>,
ml_weights: Option<Array1<f64>>,
join_correlations: HashMap<(String, String), f64>,
#[serde(skip)]
estimation_cache: CardinalityCache,
#[serde(skip)]
cache_hits: usize,
#[serde(skip)]
cache_misses: usize,
}
impl CardinalityEstimator {
pub fn new() -> Self {
Self::with_method(EstimationMethod::Simple)
}
pub fn with_method(method: EstimationMethod) -> Self {
Self::with_method_and_cache_size(method, 1000)
}
pub fn with_method_and_cache_size(method: EstimationMethod, cache_size: usize) -> Self {
Self {
method,
pattern_stats: HashMap::new(),
histograms: HashMap::new(),
sketches: HashMap::new(),
samples: HashMap::new(),
ml_weights: None,
join_correlations: HashMap::new(),
estimation_cache: CardinalityCache::new(cache_size),
cache_hits: 0,
cache_misses: 0,
}
}
pub fn update_statistics(
&mut self,
predicate: String,
count: u64,
distinct_subjects: u64,
distinct_objects: u64,
) {
let selectivity = if count > 0 {
(distinct_subjects * distinct_objects) as f64 / (count * count) as f64
} else {
1.0
};
self.pattern_stats.insert(
predicate.clone(),
PatternStatistics {
count,
distinct_subjects,
distinct_objects,
selectivity,
},
);
self.estimation_cache.clear();
}
pub fn add_histogram(&mut self, histogram: PredicateHistogram) {
self.histograms
.insert(histogram.predicate.clone(), histogram);
}
pub fn add_sketch(&mut self, predicate: String, sketch: HyperLogLogSketch) {
self.sketches.insert(predicate, sketch);
}
pub fn add_sample(&mut self, predicate: String, sample: ReservoirSample) {
self.samples.insert(predicate, sample);
}
pub fn estimate_pattern_cardinality(&mut self, pattern: &TriplePattern) -> Result<u64> {
let cache_key = self.pattern_to_cache_key(pattern);
if let Some(cached_cardinality) = self.estimation_cache.get(&cache_key) {
self.cache_hits += 1;
return Ok(cached_cardinality);
}
self.cache_misses += 1;
let cardinality = match self.method {
EstimationMethod::Simple => self.estimate_simple(pattern)?,
EstimationMethod::Histogram => self.estimate_histogram(pattern)?,
EstimationMethod::Sampling => self.estimate_sampling(pattern)?,
EstimationMethod::Sketch => self.estimate_sketch(pattern)?,
EstimationMethod::MachineLearning => self.estimate_ml(pattern)?,
};
self.estimation_cache.insert(cache_key, cardinality);
Ok(cardinality)
}
fn pattern_to_cache_key(&self, pattern: &TriplePattern) -> String {
format!(
"{}:{}:{}",
self.term_to_key_part(&pattern.subject),
self.term_to_key_part(&pattern.predicate),
self.term_to_key_part(&pattern.object),
)
}
fn term_to_key_part(&self, term: &crate::algebra::Term) -> String {
match term {
crate::algebra::Term::Variable(v) => format!("?{}", v.as_str()),
crate::algebra::Term::Iri(iri) => format!("I:{}", iri.as_str()),
crate::algebra::Term::Literal(lit) => format!("L:{}", lit),
crate::algebra::Term::BlankNode(bn) => format!("B:{}", bn.as_str()),
crate::algebra::Term::QuotedTriple(triple) => {
format!(
"Q:{}:{}:{}",
self.term_to_key_part(&triple.subject),
self.term_to_key_part(&triple.predicate),
self.term_to_key_part(&triple.object)
)
}
crate::algebra::Term::PropertyPath(path) => format!("P:{:?}", path),
}
}
pub fn cache_statistics(&self) -> (usize, usize, usize, f64) {
let hits = self.cache_hits;
let misses = self.cache_misses;
let size = self.estimation_cache.len();
let hit_rate = if hits + misses > 0 {
hits as f64 / (hits + misses) as f64
} else {
0.0
};
(hits, misses, size, hit_rate)
}
pub fn clear_cache(&mut self) {
self.estimation_cache.clear();
self.cache_hits = 0;
self.cache_misses = 0;
}
fn estimate_simple(&self, pattern: &TriplePattern) -> Result<u64> {
let predicate_iri = match &pattern.predicate {
crate::algebra::Term::Iri(iri) => iri.as_str().to_string(),
_ => return Ok(1000), };
let stats = self
.pattern_stats
.get(&predicate_iri)
.ok_or_else(|| EstimationError::NoStatistics(predicate_iri.clone()))?;
let mut selectivity = 1.0;
if matches!(
pattern.subject,
crate::algebra::Term::Iri(_) | crate::algebra::Term::Literal(_)
) {
selectivity *= 1.0 / stats.distinct_subjects as f64;
}
selectivity *= stats.selectivity;
if matches!(
pattern.object,
crate::algebra::Term::Iri(_) | crate::algebra::Term::Literal(_)
) {
selectivity *= 1.0 / stats.distinct_objects as f64;
}
Ok((stats.count as f64 * selectivity).max(1.0) as u64)
}
fn estimate_histogram(&self, pattern: &TriplePattern) -> Result<u64> {
let predicate_iri = match &pattern.predicate {
crate::algebra::Term::Iri(iri) => iri.as_str().to_string(),
_ => return self.estimate_simple(pattern), };
let histogram = self
.histograms
.get(&predicate_iri)
.ok_or_else(|| EstimationError::NoStatistics(predicate_iri.clone()))?;
let base_cardinality = histogram.total_count as f64;
Ok(base_cardinality.max(1.0) as u64)
}
fn estimate_sampling(&self, pattern: &TriplePattern) -> Result<u64> {
let predicate_iri = match &pattern.predicate {
crate::algebra::Term::Iri(iri) => iri.as_str().to_string(),
_ => return self.estimate_simple(pattern),
};
let sample = self
.samples
.get(&predicate_iri)
.ok_or_else(|| EstimationError::NoStatistics(predicate_iri.clone()))?;
let distinct_estimate = sample.estimate_distinct();
Ok(distinct_estimate)
}
fn estimate_sketch(&self, pattern: &TriplePattern) -> Result<u64> {
let predicate_iri = match &pattern.predicate {
crate::algebra::Term::Iri(iri) => iri.as_str().to_string(),
_ => return self.estimate_simple(pattern),
};
let sketch = self
.sketches
.get(&predicate_iri)
.ok_or_else(|| EstimationError::NoStatistics(predicate_iri.clone()))?;
Ok(sketch.estimate_cardinality())
}
fn estimate_ml(&self, pattern: &TriplePattern) -> Result<u64> {
let features = self.extract_pattern_features(pattern);
if let Some(ref weights) = self.ml_weights {
let mut prediction = 0.0;
for (i, &feature) in features.iter().enumerate() {
if i < weights.len() {
prediction += weights[i] * feature;
}
}
Ok(prediction.max(1.0) as u64)
} else {
self.estimate_simple(pattern)
}
}
fn extract_pattern_features(&self, pattern: &TriplePattern) -> Vec<f64> {
let mut features = vec![0.0; 10];
features[0] = if matches!(
pattern.subject,
crate::algebra::Term::Iri(_) | crate::algebra::Term::Literal(_)
) {
1.0
} else {
0.0
};
features[1] = if matches!(pattern.predicate, crate::algebra::Term::Iri(_)) {
1.0
} else {
0.0
};
features[2] = if matches!(
pattern.object,
crate::algebra::Term::Iri(_) | crate::algebra::Term::Literal(_)
) {
1.0
} else {
0.0
};
if let crate::algebra::Term::Iri(iri) = &pattern.predicate {
let iri_str = iri.as_str().to_string();
if let Some(stats) = self.pattern_stats.get(&iri_str) {
features[3] = (stats.count as f64).ln();
features[4] = (stats.distinct_subjects as f64).ln();
features[5] = (stats.distinct_objects as f64).ln();
}
}
features[6] = self.estimate_simple(pattern).unwrap_or(1000) as f64;
features[7] = 0.0;
features[8] = 0.0;
features[9] = 0.0;
features
}
pub fn train_ml_model(&mut self, training_data: &[(TriplePattern, u64)]) {
if training_data.is_empty() {
return;
}
if self.ml_weights.is_none() {
self.ml_weights = Some(Array1::from_vec(vec![1.0; 10]));
}
let training_features: Vec<(Vec<f64>, u64)> = training_data
.iter()
.map(|(pattern, cardinality)| (self.extract_pattern_features(pattern), *cardinality))
.collect();
let weights = self
.ml_weights
.as_mut()
.expect("Weights should be initialized");
let learning_rate = 0.01;
let num_iterations = 100;
for _ in 0..num_iterations {
let mut gradients: Array1<f64> = Array1::zeros(10);
for (features, actual_cardinality) in &training_features {
let features_array = Array1::from_vec(features.clone());
let prediction = weights.dot(&features_array);
let error = prediction - *actual_cardinality as f64;
for i in 0..10 {
gradients[i] += error * features_array[i];
}
}
let n = training_features.len() as f64;
for i in 0..10 {
weights[i] -= learning_rate * gradients[i] / n;
}
}
}
pub fn estimate_join_cardinality(
&mut self,
left: &TriplePattern,
right: &TriplePattern,
) -> Result<u64> {
let left_card = self.estimate_pattern_cardinality(left)?;
let right_card = self.estimate_pattern_cardinality(right)?;
let common_vars = self.find_common_variables(left, right);
if common_vars.is_empty() {
return Ok(left_card * right_card);
}
if let (crate::algebra::Term::Iri(left_pred), crate::algebra::Term::Iri(right_pred)) =
(&left.predicate, &right.predicate)
{
let left_pred_str = left_pred.as_str().to_string();
let right_pred_str = right_pred.as_str().to_string();
let correlation = self
.join_correlations
.get(&(left_pred_str.clone(), right_pred_str.clone()))
.or_else(|| {
self.join_correlations
.get(&(right_pred_str.clone(), left_pred_str.clone()))
})
.copied()
.unwrap_or(0.1);
let base_estimate = (left_card as f64 * right_card as f64).sqrt();
return Ok((base_estimate * correlation) as u64);
}
let join_selectivity = 0.1; Ok(((left_card as f64 * right_card as f64).sqrt() * join_selectivity) as u64)
}
fn find_common_variables(&self, left: &TriplePattern, right: &TriplePattern) -> Vec<String> {
let mut common = Vec::new();
let left_vars = self.extract_variables(left);
let right_vars = self.extract_variables(right);
for var in left_vars {
if right_vars.contains(&var) {
common.push(var);
}
}
common
}
fn extract_variables(&self, pattern: &TriplePattern) -> Vec<String> {
let mut vars = Vec::new();
if let crate::algebra::Term::Variable(v) = &pattern.subject {
vars.push(v.name().to_string());
}
if let crate::algebra::Term::Variable(v) = &pattern.predicate {
vars.push(v.name().to_string());
}
if let crate::algebra::Term::Variable(v) = &pattern.object {
vars.push(v.name().to_string());
}
vars
}
pub fn update_join_correlation(&mut self, pred1: String, pred2: String, correlation: f64) {
self.join_correlations.insert((pred1, pred2), correlation);
}
pub fn statistics_summary(&self) -> String {
format!(
"CardinalityEstimator{{ method: {:?}, patterns: {}, histograms: {}, sketches: {}, samples: {}, ml_trained: {} }}",
self.method,
self.pattern_stats.len(),
self.histograms.len(),
self.sketches.len(),
self.samples.len(),
self.ml_weights.is_some()
)
}
}
impl Default for CardinalityEstimator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::algebra::{Term, Variable};
#[test]
fn test_hyperloglog_basic() {
let mut hll = HyperLogLogSketch::new(14);
for i in 0..1000 {
hll.add(&format!("element_{}", i));
}
let estimated = hll.estimate_cardinality();
assert!((900..=1100).contains(&estimated));
}
#[test]
fn test_hyperloglog_duplicates() {
let mut hll = HyperLogLogSketch::new(14);
for _rep in 0..10 {
for i in 0..100 {
hll.add(&format!("element_{}", i));
}
}
let estimated = hll.estimate_cardinality();
assert!((80..=120).contains(&estimated));
}
#[test]
fn test_hyperloglog_merge() {
let mut hll1 = HyperLogLogSketch::new(14);
let mut hll2 = HyperLogLogSketch::new(14);
for i in 0..500 {
hll1.add(&format!("element_{}", i));
}
for i in 500..1000 {
hll2.add(&format!("element_{}", i));
}
hll1.merge(&hll2);
let estimated = hll1.estimate_cardinality();
assert!((900..=1100).contains(&estimated));
}
#[test]
fn test_reservoir_sample_basic() {
let mut reservoir = ReservoirSample::new(100);
for i in 0..1000 {
reservoir.add(format!("element_{}", i));
}
assert_eq!(reservoir.samples.len(), 100);
assert_eq!(reservoir.elements_seen, 1000);
assert_eq!(reservoir.estimate_distinct(), 1000);
}
#[test]
fn test_reservoir_sample_duplicates() {
let mut reservoir = ReservoirSample::new(100);
for _rep in 0..10 {
for i in 0..100 {
reservoir.add(format!("element_{}", i));
}
}
let estimated = reservoir.estimate_distinct();
assert!(
(50..=1000).contains(&estimated),
"Expected 50-1000, got {}",
estimated
);
}
#[test]
fn test_simple_estimation() {
let mut estimator = CardinalityEstimator::new();
estimator.update_statistics("http://xmlns.com/foaf/0.1/name".to_string(), 1000, 800, 900);
let pattern = TriplePattern {
subject: Term::Variable(Variable::new("s").expect("Valid variable name")),
predicate: Term::Iri(
oxirs_core::NamedNode::new("http://xmlns.com/foaf/0.1/name").expect("Valid IRI"),
),
object: Term::Variable(Variable::new("o").expect("Valid variable name")),
};
let estimated = estimator.estimate_pattern_cardinality(&pattern).unwrap();
assert!((500..=1500).contains(&estimated));
}
#[test]
fn test_bound_pattern_estimation() {
let mut estimator = CardinalityEstimator::new();
estimator.update_statistics("http://xmlns.com/foaf/0.1/name".to_string(), 1000, 800, 900);
let pattern = TriplePattern {
subject: Term::Iri(
oxirs_core::NamedNode::new("http://example.org/person1").expect("Valid IRI"),
),
predicate: Term::Iri(
oxirs_core::NamedNode::new("http://xmlns.com/foaf/0.1/name").expect("Valid IRI"),
),
object: Term::Variable(Variable::new("name").expect("Valid variable name")),
};
let estimated = estimator.estimate_pattern_cardinality(&pattern).unwrap();
assert!(estimated < 10);
}
#[test]
fn test_join_cardinality_estimation() {
let mut estimator = CardinalityEstimator::new();
estimator.update_statistics("http://xmlns.com/foaf/0.1/name".to_string(), 1000, 800, 900);
estimator.update_statistics(
"http://xmlns.com/foaf/0.1/knows".to_string(),
5000,
800,
800,
);
let left = TriplePattern {
subject: Term::Variable(Variable::new("person").expect("Valid variable name")),
predicate: Term::Iri(
oxirs_core::NamedNode::new("http://xmlns.com/foaf/0.1/name").expect("Valid IRI"),
),
object: Term::Variable(Variable::new("name").expect("Valid variable name")),
};
let right = TriplePattern {
subject: Term::Variable(Variable::new("person").expect("Valid variable name")),
predicate: Term::Iri(
oxirs_core::NamedNode::new("http://xmlns.com/foaf/0.1/knows").expect("Valid IRI"),
),
object: Term::Variable(Variable::new("friend").expect("Valid variable name")),
};
let estimated = estimator.estimate_join_cardinality(&left, &right).unwrap();
assert!(estimated < 5000, "Expected < 5000, got {}", estimated);
assert!(estimated > 10, "Expected > 10, got {}", estimated);
}
#[test]
fn test_histogram_selectivity() {
let mut histogram = PredicateHistogram::new("http://example.org/price".to_string(), 10);
for i in 0..10 {
histogram.object_buckets.push(HistogramBucket {
lower_bound: i as f64 * 10.0,
upper_bound: (i + 1) as f64 * 10.0,
count: 100,
distinct_count: 100,
});
}
histogram.total_count = 1000;
let selectivity = histogram.estimate_selectivity(20.0, 30.0, false);
assert!((selectivity - 0.1).abs() < 0.01);
}
#[test]
fn test_ml_training() {
let mut estimator = CardinalityEstimator::with_method(EstimationMethod::MachineLearning);
let pattern1 = TriplePattern {
subject: Term::Variable(Variable::new("s").expect("Valid variable name")),
predicate: Term::Iri(
oxirs_core::NamedNode::new("http://example.org/pred1").expect("Valid IRI"),
),
object: Term::Variable(Variable::new("o").expect("Valid variable name")),
};
let training_data = vec![(pattern1, 500)];
estimator.train_ml_model(&training_data);
assert!(estimator.ml_weights.is_some());
assert_eq!(estimator.ml_weights.as_ref().unwrap().len(), 10);
}
}