use super::constellation::{ConstellationMap, Peak};
use std::collections::HashSet;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Hash {
value: u64,
}
impl Hash {
#[must_use]
pub fn from_peaks(anchor: &Peak, target: &Peak) -> Self {
let f1 = anchor.quantized_frequency();
let f2 = target.quantized_frequency();
let time_delta = target
.quantized_time()
.saturating_sub(anchor.quantized_time());
let hash_value = ((u64::from(f1) & 0x0F_FFFF) << 44)
| ((u64::from(f2) & 0x0F_FFFF) << 24)
| (u64::from(time_delta) & 0x00FF_FFFF);
Self { value: hash_value }
}
#[must_use]
pub fn from_components(freq1: u32, freq2: u32, time_delta: u32) -> Self {
let hash_value = ((u64::from(freq1) & 0x0F_FFFF) << 44)
| ((u64::from(freq2) & 0x0F_FFFF) << 24)
| (u64::from(time_delta) & 0x00FF_FFFF);
Self { value: hash_value }
}
#[must_use]
pub const fn value(&self) -> u64 {
self.value
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub const fn freq1(&self) -> u32 {
((self.value >> 44) & 0x0F_FFFF) as u32
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub const fn freq2(&self) -> u32 {
((self.value >> 24) & 0x0F_FFFF) as u32
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub const fn time_delta(&self) -> u32 {
(self.value & 0x00FF_FFFF) as u32
}
#[must_use]
pub const fn to_bytes(&self) -> [u8; 8] {
self.value.to_le_bytes()
}
#[must_use]
pub const fn from_bytes(bytes: [u8; 8]) -> Self {
Self {
value: u64::from_le_bytes(bytes),
}
}
}
impl From<u64> for Hash {
fn from(value: u64) -> Self {
Self { value }
}
}
impl From<Hash> for u64 {
fn from(hash: Hash) -> Self {
hash.value
}
}
pub struct HashGenerator {
target_zone_size: usize,
target_zone_offset: usize,
num_targets_per_anchor: usize,
}
impl HashGenerator {
#[must_use]
pub const fn new(
target_zone_size: usize,
target_zone_offset: usize,
num_targets_per_anchor: usize,
) -> Self {
Self {
target_zone_size,
target_zone_offset,
num_targets_per_anchor,
}
}
#[allow(clippy::cast_precision_loss)]
pub fn generate(&self, constellation: &ConstellationMap) -> Vec<(Hash, f64)> {
let mut hashes = Vec::new();
let mut seen_hashes = HashSet::new();
for anchor in &constellation.peaks {
let zone_start = self.target_zone_offset as f64 * 0.01; let zone_end = (self.target_zone_offset + self.target_zone_size) as f64 * 0.01;
let targets = constellation.nearest_peaks(
anchor,
(zone_start, zone_end),
self.num_targets_per_anchor,
);
for target in targets {
let hash = Hash::from_peaks(anchor, target);
if seen_hashes.insert(hash) {
hashes.push((hash, anchor.time));
}
}
}
hashes
}
#[allow(clippy::cast_precision_loss)]
pub fn generate_filtered(
&self,
constellation: &ConstellationMap,
min_time_delta: f64,
max_time_delta: f64,
) -> Vec<(Hash, f64)> {
let mut hashes = Vec::new();
let mut seen_hashes = HashSet::new();
for anchor in &constellation.peaks {
let zone_start = min_time_delta;
let zone_end = max_time_delta;
let targets = constellation.nearest_peaks(
anchor,
(zone_start, zone_end),
self.num_targets_per_anchor,
);
for target in targets {
let time_diff = target.time - anchor.time;
if time_diff >= min_time_delta && time_diff <= max_time_delta {
let hash = Hash::from_peaks(anchor, target);
if seen_hashes.insert(hash) {
hashes.push((hash, anchor.time));
}
}
}
}
hashes
}
#[allow(clippy::cast_precision_loss)]
pub fn generate_batched(
&self,
constellation: &ConstellationMap,
batch_size: usize,
) -> Vec<Vec<(Hash, f64)>> {
let mut batches = Vec::new();
let mut current_batch = Vec::new();
let mut seen_hashes = HashSet::new();
let zone_start = self.target_zone_offset as f64 * 0.01;
let zone_end = (self.target_zone_offset + self.target_zone_size) as f64 * 0.01;
for anchor in &constellation.peaks {
let targets = constellation.nearest_peaks(
anchor,
(zone_start, zone_end),
self.num_targets_per_anchor,
);
for target in targets {
let hash = Hash::from_peaks(anchor, target);
if seen_hashes.insert(hash) {
current_batch.push((hash, anchor.time));
if current_batch.len() >= batch_size {
batches.push(current_batch);
current_batch = Vec::new();
}
}
}
}
if !current_batch.is_empty() {
batches.push(current_batch);
}
batches
}
#[must_use]
pub fn estimate_hash_count(&self, peak_count: usize) -> usize {
peak_count * self.num_targets_per_anchor
}
#[must_use]
pub fn statistics(hashes: &[(Hash, f64)]) -> HashStatistics {
if hashes.is_empty() {
return HashStatistics::default();
}
let unique_hashes: HashSet<Hash> = hashes.iter().map(|(h, _)| *h).collect();
let min_time = hashes
.iter()
.map(|(_, t)| *t)
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0.0);
let max_time = hashes
.iter()
.map(|(_, t)| *t)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0.0);
let time_deltas: Vec<u32> = hashes.iter().map(|(h, _)| h.time_delta()).collect();
let avg_time_delta = if !time_deltas.is_empty() {
time_deltas.iter().map(|&x| u64::from(x)).sum::<u64>() / time_deltas.len() as u64
} else {
0
};
HashStatistics {
total_count: hashes.len(),
unique_count: unique_hashes.len(),
time_range: (min_time, max_time),
avg_time_delta: avg_time_delta as u32,
collision_rate: if hashes.len() > 0 {
1.0 - (unique_hashes.len() as f64 / hashes.len() as f64)
} else {
0.0
},
}
}
}
impl Default for HashGenerator {
fn default() -> Self {
Self::new(32, 1, 5)
}
}
#[derive(Clone, Debug, Default)]
pub struct HashStatistics {
pub total_count: usize,
pub unique_count: usize,
pub time_range: (f64, f64),
pub avg_time_delta: u32,
pub collision_rate: f64,
}
impl HashStatistics {
#[must_use]
pub fn is_good_quality(&self) -> bool {
self.unique_count > 100 && self.collision_rate < 0.5
}
#[must_use]
pub fn density(&self) -> f64 {
let duration = self.time_range.1 - self.time_range.0;
if duration > 0.0 {
self.total_count as f64 / duration
} else {
0.0
}
}
}
pub struct HashComparison;
impl HashComparison {
#[must_use]
pub fn hamming_distance(h1: Hash, h2: Hash) -> u32 {
(h1.value() ^ h2.value()).count_ones()
}
#[must_use]
pub fn are_similar(h1: Hash, h2: Hash, max_distance: u32) -> bool {
Self::hamming_distance(h1, h2) <= max_distance
}
#[must_use]
pub fn find_similar(
query: Hash,
candidates: &[(Hash, f64)],
max_distance: u32,
) -> Vec<&(Hash, f64)> {
candidates
.iter()
.filter(|(h, _)| Self::are_similar(query, *h, max_distance))
.collect()
}
#[must_use]
pub fn similarity_ratio(set1: &[(Hash, f64)], set2: &[(Hash, f64)]) -> f64 {
if set1.is_empty() || set2.is_empty() {
return 0.0;
}
let hashes1: HashSet<Hash> = set1.iter().map(|(h, _)| *h).collect();
let hashes2: HashSet<Hash> = set2.iter().map(|(h, _)| *h).collect();
let intersection = hashes1.intersection(&hashes2).count();
let union = hashes1.union(&hashes2).count();
if union > 0 {
intersection as f64 / union as f64
} else {
0.0
}
}
}