use crate::consolidation::{ConsolidatedEntry, ConsolidationPolicy, ConsolidationResult};
use crate::delta::{Delta, DeltaType};
use crate::error::Result;
use crate::hash_chain::HashChain;
use crate::plasticity::PlasticityRule;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use ternary_signal::{PackedSignal, Signal};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Hash)]
pub enum ThermalState {
#[default]
Hot,
Warm,
Cool,
Cold,
}
impl ThermalState {
pub fn default_decay_rate(&self) -> Signal {
match self {
Self::Hot => Signal::positive(26), Self::Warm => Signal::positive(3), Self::Cool => Signal::positive(1), Self::Cold => Signal::positive(1), }
}
pub fn promotion_threshold(&self) -> Signal {
match self {
Self::Hot => Signal::positive(153), Self::Warm => Signal::positive(191), Self::Cool => Signal::positive(230), Self::Cold => Signal::positive(255), }
}
pub fn demotion_threshold(&self) -> Signal {
match self {
Self::Hot => Signal::positive(0), Self::Warm => Signal::positive(77), Self::Cool => Signal::positive(102), Self::Cold => Signal::positive(128), }
}
pub fn min_observations_for_promotion(&self) -> usize {
match self {
Self::Hot => 3,
Self::Warm => 10,
Self::Cool => 50,
Self::Cold => usize::MAX,
}
}
pub fn colder(&self) -> Option<Self> {
match self {
Self::Hot => Some(Self::Warm),
Self::Warm => Some(Self::Cool),
Self::Cool => Some(Self::Cold),
Self::Cold => None,
}
}
pub fn hotter(&self) -> Option<Self> {
match self {
Self::Hot => None,
Self::Warm => Some(Self::Hot),
Self::Cool => Some(Self::Warm),
Self::Cold => Some(Self::Cool),
}
}
pub fn index(&self) -> usize {
match self {
Self::Hot => 0,
Self::Warm => 1,
Self::Cool => 2,
Self::Cold => 3,
}
}
pub fn from_index(index: usize) -> Option<Self> {
match index {
0 => Some(Self::Hot),
1 => Some(Self::Warm),
2 => Some(Self::Cool),
3 => Some(Self::Cold),
_ => None,
}
}
pub fn all() -> [Self; 4] {
[Self::Hot, Self::Warm, Self::Cool, Self::Cold]
}
pub fn name(&self) -> &'static str {
match self {
Self::Hot => "hot",
Self::Warm => "warm",
Self::Cool => "cool",
Self::Cold => "cold",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThermalConfig {
pub decay_rates: [Signal; 4],
pub promotion_thresholds: [Signal; 4],
pub demotion_thresholds: [Signal; 4],
pub min_observations: [usize; 4],
pub allow_demotion: [bool; 4],
pub prune_threshold: Signal,
pub crystallization_threshold: Signal,
pub allow_warming: bool,
pub warming_delta: Signal,
}
impl Default for ThermalConfig {
fn default() -> Self {
Self {
decay_rates: [
Signal::positive(26), Signal::positive(3), Signal::positive(1), Signal::positive(1), ],
promotion_thresholds: [
Signal::positive(153), Signal::positive(191), Signal::positive(230), Signal::positive(255), ],
demotion_thresholds: [
Signal::positive(0), Signal::positive(77), Signal::positive(102), Signal::positive(128), ],
min_observations: [3, 10, 50, usize::MAX],
allow_demotion: [false, true, true, true],
prune_threshold: Signal::positive(13), crystallization_threshold: Signal::positive(191), allow_warming: true,
warming_delta: Signal::positive(77), }
}
}
impl ThermalConfig {
pub fn fast_learner() -> Self {
Self {
decay_rates: [
Signal::positive(13), Signal::positive(1), Signal::positive(1), Signal::positive(1), ],
promotion_thresholds: [
Signal::positive(128), Signal::positive(166), Signal::positive(217), Signal::positive(255), ],
demotion_thresholds: [
Signal::positive(0), Signal::positive(51), Signal::positive(77), Signal::positive(102), ],
min_observations: [2, 5, 20, usize::MAX],
allow_demotion: [false, true, true, false],
prune_threshold: Signal::positive(8), crystallization_threshold: Signal::positive(217), allow_warming: true,
warming_delta: Signal::positive(51), }
}
pub fn organic() -> Self {
Self {
decay_rates: [
Signal::positive(26), Signal::positive(3), Signal::positive(1), Signal::positive(1), ],
promotion_thresholds: [
Signal::positive(179), Signal::positive(204), Signal::positive(242), Signal::positive(255), ],
demotion_thresholds: [
Signal::positive(0), Signal::positive(64), Signal::positive(89), Signal::positive(115), ],
min_observations: [5, 15, 100, usize::MAX],
allow_demotion: [false, true, true, true],
prune_threshold: Signal::positive(13), crystallization_threshold: Signal::positive(242), allow_warming: true,
warming_delta: Signal::positive(77), }
}
pub fn decay_rate(&self, state: ThermalState) -> Signal {
self.decay_rates[state.index()]
}
pub fn promotion_threshold(&self, state: ThermalState) -> Signal {
self.promotion_thresholds[state.index()]
}
pub fn demotion_threshold(&self, state: ThermalState) -> Signal {
self.demotion_thresholds[state.index()]
}
pub fn min_obs(&self, state: ThermalState) -> usize {
self.min_observations[state.index()]
}
pub fn can_demote(&self, state: ThermalState) -> bool {
self.allow_demotion[state.index()]
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Thermogram {
pub id: String,
pub name: String,
#[serde(default)]
pub hot_entries: HashMap<String, ConsolidatedEntry>,
#[serde(default)]
pub warm_entries: HashMap<String, ConsolidatedEntry>,
#[serde(default)]
pub cool_entries: HashMap<String, ConsolidatedEntry>,
#[serde(default)]
pub cold_entries: HashMap<String, ConsolidatedEntry>,
pub dirty_chain: HashChain,
pub plasticity_rule: PlasticityRule,
pub consolidation_policy: ConsolidationPolicy,
pub thermal_config: ThermalConfig,
pub metadata: ThermogramMetadata,
}
#[derive(Debug, Clone, Default)]
pub struct CrystallizationResult {
pub crystallized: usize,
pub merged: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThermogramMetadata {
pub created_at: DateTime<Utc>,
pub last_consolidation: DateTime<Utc>,
pub total_deltas: usize,
pub total_consolidations: usize,
#[serde(default)]
pub custom: Option<Vec<u8>>,
}
impl Thermogram {
pub fn new(name: impl Into<String>, plasticity_rule: PlasticityRule) -> Self {
let now = Utc::now();
Self {
id: uuid::Uuid::new_v4().to_string(),
name: name.into(),
hot_entries: HashMap::new(),
warm_entries: HashMap::new(),
cool_entries: HashMap::new(),
cold_entries: HashMap::new(),
dirty_chain: HashChain::new(),
plasticity_rule,
consolidation_policy: ConsolidationPolicy::default(),
thermal_config: ThermalConfig::default(),
metadata: ThermogramMetadata {
created_at: now,
last_consolidation: now,
total_deltas: 0,
total_consolidations: 0,
custom: None,
},
}
}
pub fn for_fast_learner(name: impl Into<String>, plasticity_rule: PlasticityRule) -> Self {
Self::with_thermal_config(name, plasticity_rule, ThermalConfig::fast_learner())
}
pub fn for_organic(name: impl Into<String>, plasticity_rule: PlasticityRule) -> Self {
Self::with_thermal_config(name, plasticity_rule, ThermalConfig::organic())
}
pub fn entries(&self, state: ThermalState) -> &HashMap<String, ConsolidatedEntry> {
match state {
ThermalState::Hot => &self.hot_entries,
ThermalState::Warm => &self.warm_entries,
ThermalState::Cool => &self.cool_entries,
ThermalState::Cold => &self.cold_entries,
}
}
pub fn entries_mut(&mut self, state: ThermalState) -> &mut HashMap<String, ConsolidatedEntry> {
match state {
ThermalState::Hot => &mut self.hot_entries,
ThermalState::Warm => &mut self.warm_entries,
ThermalState::Cool => &mut self.cool_entries,
ThermalState::Cold => &mut self.cold_entries,
}
}
pub fn total_entries(&self) -> usize {
self.hot_entries.len()
+ self.warm_entries.len()
+ self.cool_entries.len()
+ self.cold_entries.len()
}
pub fn with_thermal_config(
name: impl Into<String>,
plasticity_rule: PlasticityRule,
thermal_config: ThermalConfig,
) -> Self {
let mut thermo = Self::new(name, plasticity_rule);
thermo.thermal_config = thermal_config;
thermo
}
pub fn apply_delta(&mut self, delta: Delta) -> Result<()> {
self.dirty_chain.append(delta)?;
self.metadata.total_deltas += 1;
if self.should_consolidate() {
self.consolidate()?;
}
Ok(())
}
pub fn read(&self, key: &str) -> Result<Option<Vec<PackedSignal>>> {
if let Some(delta) = self.dirty_chain.get_latest(key) {
match delta.delta_type {
DeltaType::Create | DeltaType::Update | DeltaType::Merge => {
return Ok(Some(delta.value.clone()));
}
DeltaType::Delete => {
return Ok(None);
}
}
}
for state in ThermalState::all() {
if let Some(entry) = self.entries(state).get(key) {
return Ok(Some(entry.value.clone()));
}
}
Ok(None)
}
pub fn read_with_strength(&self, key: &str) -> Result<Option<(Vec<PackedSignal>, Signal)>> {
if let Some(delta) = self.dirty_chain.get_latest(key) {
match delta.delta_type {
DeltaType::Create | DeltaType::Update | DeltaType::Merge => {
return Ok(Some((delta.value.clone(), delta.metadata.strength)));
}
DeltaType::Delete => {
return Ok(None);
}
}
}
for state in ThermalState::all() {
if let Some(entry) = self.entries(state).get(key) {
return Ok(Some((entry.value.clone(), entry.strength)));
}
}
Ok(None)
}
pub fn read_with_state(
&self,
key: &str,
) -> Result<Option<(Vec<PackedSignal>, Signal, ThermalState)>> {
if let Some(delta) = self.dirty_chain.get_latest(key) {
match delta.delta_type {
DeltaType::Create | DeltaType::Update | DeltaType::Merge => {
return Ok(Some((
delta.value.clone(),
delta.metadata.strength,
ThermalState::Hot,
)));
}
DeltaType::Delete => {
return Ok(None);
}
}
}
for state in ThermalState::all() {
if let Some(entry) = self.entries(state).get(key) {
return Ok(Some((entry.value.clone(), entry.strength, state)));
}
}
Ok(None)
}
pub fn keys(&self) -> Vec<String> {
let mut keys: std::collections::HashSet<String> = std::collections::HashSet::new();
for state in ThermalState::all() {
for key in self.entries(state).keys() {
keys.insert(key.clone());
}
}
for delta in &self.dirty_chain.deltas {
if delta.delta_type != DeltaType::Delete {
keys.insert(delta.key.clone());
}
}
keys.into_iter().collect()
}
pub fn keys_for_state(&self, state: ThermalState) -> Vec<String> {
self.entries(state).keys().cloned().collect()
}
pub fn hot_keys(&self) -> Vec<String> {
self.keys_for_state(ThermalState::Hot)
}
pub fn warm_keys(&self) -> Vec<String> {
self.keys_for_state(ThermalState::Warm)
}
pub fn cool_keys(&self) -> Vec<String> {
self.keys_for_state(ThermalState::Cool)
}
pub fn cold_keys(&self) -> Vec<String> {
self.keys_for_state(ThermalState::Cold)
}
pub fn history(&self, key: &str) -> Vec<&Delta> {
self.dirty_chain.get_history(key)
}
pub fn should_consolidate(&self) -> bool {
let dirty_size = self.dirty_chain.len();
let dirty_bytes = self.estimate_dirty_size();
self.consolidation_policy.should_consolidate(
dirty_size,
&self.metadata.last_consolidation,
dirty_bytes,
)
}
pub fn consolidate(&mut self) -> Result<ConsolidationResult> {
let (new_hot_state, mut result) = crate::consolidation::consolidate(
&self.dirty_chain.deltas,
&self.hot_entries,
&self.plasticity_rule,
&self.consolidation_policy,
)?;
self.hot_entries = new_hot_state;
self.dirty_chain = HashChain::new();
self.metadata.last_consolidation = Utc::now();
self.metadata.total_consolidations += 1;
let crystal_result = self.crystallize()?;
result.entries_merged += crystal_result.crystallized;
Ok(result)
}
pub fn crystallize(&mut self) -> Result<CrystallizationResult> {
let mut result = CrystallizationResult::default();
let mut keys_to_crystallize = Vec::new();
let min_obs = self.thermal_config.min_observations[ThermalState::Hot.index()];
let threshold_mag = self.thermal_config.crystallization_threshold.magnitude;
for (key, entry) in &self.hot_entries {
if entry.strength.magnitude >= threshold_mag && entry.update_count >= min_obs {
keys_to_crystallize.push(key.clone());
}
}
for key in keys_to_crystallize {
if let Some(entry) = self.hot_entries.remove(&key) {
if let Some(cold_entry) = self.cold_entries.get_mut(&key) {
cold_entry.strength = cold_entry
.strength
.scale(0.3)
.add(&entry.strength.scale(0.7));
cold_entry.value = entry.value;
cold_entry.updated_at = entry.updated_at;
cold_entry.update_count += entry.update_count;
result.merged += 1;
} else {
self.cold_entries.insert(key, entry);
result.crystallized += 1;
}
}
}
Ok(result)
}
pub fn run_thermal_transitions(&mut self) -> Result<()> {
self.promote_layer(ThermalState::Hot, ThermalState::Warm)?;
self.promote_layer(ThermalState::Warm, ThermalState::Cool)?;
self.promote_layer(ThermalState::Cool, ThermalState::Cold)?;
if self.thermal_config.can_demote(ThermalState::Cold) {
self.demote_layer(ThermalState::Cold, ThermalState::Cool)?;
}
if self.thermal_config.can_demote(ThermalState::Cool) {
self.demote_layer(ThermalState::Cool, ThermalState::Warm)?;
}
if self.thermal_config.can_demote(ThermalState::Warm) {
self.demote_layer(ThermalState::Warm, ThermalState::Hot)?;
}
self.prune_all_layers()?;
Ok(())
}
fn promote_layer(&mut self, from: ThermalState, to: ThermalState) -> Result<usize> {
let threshold_mag = self.thermal_config.promotion_threshold(from).magnitude;
let min_obs = self.thermal_config.min_obs(from);
let keys_to_promote: Vec<String> = self
.entries(from)
.iter()
.filter(|(_, entry)| {
entry.strength.magnitude >= threshold_mag && entry.update_count >= min_obs
})
.map(|(k, _)| k.clone())
.collect();
let count = keys_to_promote.len();
for key in keys_to_promote {
if let Some(entry) = self.entries_mut(from).remove(&key) {
if let Some(existing) = self.entries_mut(to).get_mut(&key) {
existing.strength =
existing.strength.scale(0.3).add(&entry.strength.scale(0.7));
existing.value = entry.value;
existing.updated_at = entry.updated_at;
existing.update_count += entry.update_count;
} else {
self.entries_mut(to).insert(key, entry);
}
}
}
Ok(count)
}
fn demote_layer(&mut self, from: ThermalState, to: ThermalState) -> Result<usize> {
let threshold_mag = self.thermal_config.demotion_threshold(from).magnitude;
let keys_to_demote: Vec<String> = self
.entries(from)
.iter()
.filter(|(_, entry)| entry.strength.magnitude < threshold_mag)
.map(|(k, _)| k.clone())
.collect();
let count = keys_to_demote.len();
for key in keys_to_demote {
if let Some(mut entry) = self.entries_mut(from).remove(&key) {
entry.strength = entry.strength.scale(0.95);
if entry.strength.magnitude == 0 {
entry.strength = Signal::positive(1);
}
entry.updated_at = Utc::now();
if let Some(existing) = self.entries_mut(to).get_mut(&key) {
if entry.strength.magnitude > existing.strength.magnitude {
existing.strength = entry.strength;
}
existing.updated_at = entry.updated_at;
} else {
self.entries_mut(to).insert(key, entry);
}
}
}
Ok(count)
}
fn prune_all_layers(&mut self) -> Result<usize> {
let threshold_mag = self.thermal_config.prune_threshold.magnitude;
let mut total = 0;
for state in ThermalState::all() {
let before = self.entries(state).len();
self.entries_mut(state)
.retain(|_, e| e.strength.magnitude >= threshold_mag);
total += before - self.entries(state).len();
}
Ok(total)
}
pub fn warm(&mut self, key: &str) -> Result<bool> {
if !self.thermal_config.allow_warming {
return Ok(false);
}
if let Some(mut entry) = self.cold_entries.remove(key) {
let warming_cost = self.thermal_config.warming_delta.magnitude_f32() * 0.1;
let new_mag = entry.strength.magnitude as f32 * (1.0 - warming_cost);
let floored = (new_mag as u8).max(1); entry.strength = Signal::new(entry.strength.polarity, floored);
entry.updated_at = Utc::now();
self.hot_entries.insert(key.to_string(), entry);
Ok(true)
} else {
Ok(false)
}
}
pub fn warm_matching<F>(&mut self, predicate: F) -> Result<usize>
where
F: Fn(&str, &ConsolidatedEntry) -> bool,
{
if !self.thermal_config.allow_warming {
return Ok(0);
}
let keys_to_warm: Vec<String> = self
.cold_entries
.iter()
.filter(|(k, v)| predicate(k, v))
.map(|(k, _)| k.clone())
.collect();
let mut warmed = 0;
for key in keys_to_warm {
if self.warm(&key)? {
warmed += 1;
}
}
Ok(warmed)
}
pub fn prune_hot(&mut self) -> usize {
let threshold_mag = self.thermal_config.prune_threshold.magnitude;
let before = self.hot_entries.len();
self.hot_entries
.retain(|_, entry| entry.strength.magnitude >= threshold_mag);
before - self.hot_entries.len()
}
fn estimate_dirty_size(&self) -> usize {
self.dirty_chain
.deltas
.iter()
.map(|d| d.value.len() + d.key.len() + 200) .sum()
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let data = crate::codec::encode(self)?;
std::fs::write(path, data)?;
Ok(())
}
pub fn load(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let data = std::fs::read(path)?;
if data.len() >= 4 && &data[0..4] == b"THRM" {
crate::codec::decode(&data)
} else {
let json = String::from_utf8(data).map_err(|e| {
crate::error::Error::Deserialization(format!("invalid UTF-8: {}", e))
})?;
let thermo: Thermogram = serde_json::from_str(&json)?;
thermo.dirty_chain.verify()?;
Ok(thermo)
}
}
pub fn stats(&self) -> ThermogramStats {
ThermogramStats {
total_keys: self.keys().len(),
hot_entries: self.hot_entries.len(),
warm_entries: self.warm_entries.len(),
cool_entries: self.cool_entries.len(),
cold_entries: self.cold_entries.len(),
dirty_deltas: self.dirty_chain.len(),
total_deltas_lifetime: self.metadata.total_deltas,
total_consolidations: self.metadata.total_consolidations,
created_at: self.metadata.created_at,
last_consolidation: self.metadata.last_consolidation,
estimated_size_bytes: self.estimate_size(),
}
}
fn estimate_size(&self) -> usize {
let mut total = 0;
for state in ThermalState::all() {
total += self
.entries(state)
.values()
.map(|e| e.value.len() + e.key.len() + 100) .sum::<usize>();
}
total + self.estimate_dirty_size()
}
pub fn apply_decay(&mut self) {
for state in ThermalState::all() {
let decay_rate = self.thermal_config.decay_rate(state);
let retention = 1.0 - decay_rate.magnitude_f32();
for entry in self.entries_mut(state).values_mut() {
entry.strength = entry.strength.decayed(retention);
}
}
}
pub fn reinforce(&mut self, key: &str, amount: Signal) -> Result<bool> {
for state in ThermalState::all() {
if let Some(entry) = self.entries_mut(state).get_mut(key) {
entry.strength = entry.strength.add(&amount);
entry.update_count += 1;
entry.updated_at = Utc::now();
return Ok(true);
}
}
Ok(false)
}
pub fn weaken(&mut self, key: &str, amount: Signal) -> Result<bool> {
for state in ThermalState::all() {
if let Some(entry) = self.entries_mut(state).get_mut(key) {
let new_mag = entry.strength.magnitude.saturating_sub(amount.magnitude);
entry.strength = if new_mag == 0 {
Signal::ZERO
} else {
Signal::new(entry.strength.polarity, new_mag)
};
entry.updated_at = Utc::now();
return Ok(true);
}
}
Ok(false)
}
}
#[derive(Debug, Clone)]
pub struct ThermogramStats {
pub total_keys: usize,
pub hot_entries: usize,
pub warm_entries: usize,
pub cool_entries: usize,
pub cold_entries: usize,
pub dirty_deltas: usize,
pub total_deltas_lifetime: usize,
pub total_consolidations: usize,
pub created_at: DateTime<Utc>,
pub last_consolidation: DateTime<Utc>,
pub estimated_size_bytes: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_thermogram() {
let thermo = Thermogram::new("test", PlasticityRule::stdp_like());
assert_eq!(thermo.name, "test");
assert!(thermo.hot_entries.is_empty());
assert!(thermo.cold_entries.is_empty());
assert!(thermo.dirty_chain.is_empty());
}
#[test]
fn test_apply_and_read() {
let mut thermo = Thermogram::new("test", PlasticityRule::stdp_like());
let ps = PackedSignal::pack(1, 100, 1);
let delta = Delta::create("key1", vec![ps], "source");
thermo.apply_delta(delta).unwrap();
let value = thermo.read("key1").unwrap();
assert_eq!(value, Some(vec![ps]));
}
#[test]
fn test_update() {
let mut thermo = Thermogram::new("test", PlasticityRule::stdp_like());
let ps1 = PackedSignal::pack(1, 100, 1);
let ps2 = PackedSignal::pack(1, 200, 1);
let delta1 = Delta::create("key1", vec![ps1], "source");
thermo.apply_delta(delta1).unwrap();
let prev_hash = thermo.dirty_chain.head_hash.clone();
let delta2 = Delta::update(
"key1",
vec![ps2],
"source",
Signal::positive(204),
prev_hash,
);
thermo.apply_delta(delta2).unwrap();
let value = thermo.read("key1").unwrap();
assert_eq!(value, Some(vec![ps2]));
}
#[test]
fn test_delete() {
let mut thermo = Thermogram::new("test", PlasticityRule::stdp_like());
let delta1 = Delta::create("key1", vec![PackedSignal::pack(1, 100, 1)], "source");
thermo.apply_delta(delta1).unwrap();
let prev_hash = thermo.dirty_chain.head_hash.clone();
let delta2 = Delta::delete("key1", "source", prev_hash);
thermo.apply_delta(delta2).unwrap();
let value = thermo.read("key1").unwrap();
assert_eq!(value, None);
}
#[test]
fn test_manual_consolidation() {
let mut thermo = Thermogram::new("test", PlasticityRule::stdp_like());
let delta = Delta::create("key1", vec![PackedSignal::pack(1, 100, 1)], "source");
thermo.apply_delta(delta).unwrap();
let result = thermo.consolidate().unwrap();
assert_eq!(result.deltas_processed, 1);
assert_eq!(thermo.hot_entries.len(), 1);
assert_eq!(thermo.dirty_chain.len(), 0);
}
#[test]
fn test_crystallization() {
let mut thermo = Thermogram::new("test", PlasticityRule::stdp_like());
thermo.thermal_config.crystallization_threshold = Signal::positive(179); thermo.thermal_config.min_observations[0] = 2;
let ps = PackedSignal::pack(1, 100, 1);
let mut delta = Delta::create("key1", vec![ps], "source");
delta.metadata.strength = Signal::positive(230); thermo.apply_delta(delta).unwrap();
thermo.consolidate().unwrap();
let prev_hash = thermo.dirty_chain.head_hash.clone();
let mut delta2 = Delta::update(
"key1",
vec![ps],
"source",
Signal::positive(230),
prev_hash,
);
delta2.metadata.strength = Signal::positive(230);
thermo.apply_delta(delta2).unwrap();
thermo.consolidate().unwrap();
assert_eq!(thermo.cold_entries.len(), 1);
assert!(thermo.hot_entries.is_empty());
}
#[test]
fn test_4temp_promotion() {
let mut thermo = Thermogram::new("test", PlasticityRule::stdp_like());
thermo.thermal_config.promotion_thresholds = [
Signal::positive(128), Signal::positive(128),
Signal::positive(128),
Signal::positive(255),
];
thermo.thermal_config.min_observations = [1, 1, 1, usize::MAX];
thermo.hot_entries.insert(
"key1".to_string(),
ConsolidatedEntry {
key: "key1".to_string(),
value: vec![PackedSignal::pack(1, 100, 1)],
strength: Signal::positive(204), updated_at: Utc::now(),
update_count: 5,
},
);
thermo.run_thermal_transitions().unwrap();
assert!(thermo.hot_entries.is_empty());
assert!(thermo.warm_entries.is_empty());
assert!(thermo.cool_entries.is_empty());
assert_eq!(thermo.cold_entries.len(), 1);
}
#[test]
fn test_4temp_demotion() {
let mut thermo = Thermogram::new("test", PlasticityRule::stdp_like());
thermo.thermal_config.demotion_thresholds = [
Signal::positive(0),
Signal::positive(0),
Signal::positive(0),
Signal::positive(128), ];
thermo.thermal_config.allow_demotion = [false, true, true, true];
thermo.cold_entries.insert(
"key1".to_string(),
ConsolidatedEntry {
key: "key1".to_string(),
value: vec![PackedSignal::pack(1, 100, 1)],
strength: Signal::positive(77), updated_at: Utc::now(),
update_count: 1,
},
);
thermo.run_thermal_transitions().unwrap();
assert!(thermo.cold_entries.is_empty());
assert_eq!(thermo.cool_entries.len(), 1);
let entry = thermo.cool_entries.get("key1").unwrap();
assert!(entry.strength.magnitude < 77);
}
#[test]
fn test_warming() {
let mut thermo = Thermogram::new("test", PlasticityRule::stdp_like());
thermo.cold_entries.insert(
"cold_key".to_string(),
ConsolidatedEntry {
key: "cold_key".to_string(),
value: vec![PackedSignal::pack(1, 100, 1)],
strength: Signal::positive(204), updated_at: Utc::now(),
update_count: 5,
},
);
let warmed = thermo.warm("cold_key").unwrap();
assert!(warmed);
assert!(thermo.cold_entries.is_empty());
assert_eq!(thermo.hot_entries.len(), 1);
}
#[test]
fn test_thermal_state_read() {
let mut thermo = Thermogram::new("test", PlasticityRule::stdp_like());
thermo.hot_entries.insert(
"hot_key".to_string(),
ConsolidatedEntry {
key: "hot_key".to_string(),
value: vec![PackedSignal::pack(1, 50, 1)],
strength: Signal::positive(128), updated_at: Utc::now(),
update_count: 1,
},
);
thermo.cold_entries.insert(
"cold_key".to_string(),
ConsolidatedEntry {
key: "cold_key".to_string(),
value: vec![PackedSignal::pack(1, 200, 1)],
strength: Signal::positive(230), updated_at: Utc::now(),
update_count: 10,
},
);
let (_, _, state) = thermo.read_with_state("hot_key").unwrap().unwrap();
assert_eq!(state, ThermalState::Hot);
let (_, _, state) = thermo.read_with_state("cold_key").unwrap().unwrap();
assert_eq!(state, ThermalState::Cold);
}
#[test]
fn test_save_load() {
use tempfile::tempdir;
let dir = tempdir().unwrap();
let path = dir.path().join("test.thermo");
let ps = PackedSignal::pack(1, 100, 1);
let mut thermo = Thermogram::new("test", PlasticityRule::stdp_like());
let delta = Delta::create("key1", vec![ps], "source");
thermo.apply_delta(delta).unwrap();
thermo.save(&path).unwrap();
let loaded = Thermogram::load(&path).unwrap();
assert_eq!(loaded.name, "test");
assert_eq!(
loaded.read("key1").unwrap(),
Some(vec![ps])
);
}
}