Skip to main content

Chronofy

Temporal validity framework for time-aware AI systems.

PyPI version License: MIT Python 3.10+

Chronofy implements the Temporal-Logical Decay Architecture (TLDA) — a three-layer neuro-symbolic framework that embeds temporal validity directly into the representation, retrieval, and reasoning layers of Retrieval-Augmented Generation (RAG) systems. It also provides a standalone suite of temporal statistical tools useful outside of RAG.

Project resources


The Problem

Current RAG systems treat all retrieved facts as equally valid regardless of when they were recorded. A clinical lab reading from yesterday and one from six months ago receive identical weight if they are semantically similar to the query. This causes temporal hallucination — plausible but factually obsolete outputs.

The same failure mode appears across domains:

Domain Stale fact Consequence
Clinical Serum potassium from 6 months ago Wrong medication dosing
Financial Market volatility from last quarter Mispriced risk
Legal Superseded regulatory ruling Non-compliant advice
Geopolitical Alliance structure from 2 years ago Incorrect threat assessment

Installation

pip install chronofy

With optional dependencies:

pip install chronofy[graph]   # NetworkX for graph-based retrieval
pip install chronofy[ml]      # PyTorch + sentence-transformers for embeddings
pip install chronofy[sl]      # Subjective Logic extensions (jsonld-ex)
pip install chronofy[all]     # Everything

Architecture

Chronofy operates as three independent but composable layers, with an optional Subjective Logic extension:

┌──────────────────────────────────────────────────────────────┐
│                     Chronofy Pipeline                        │
├───────────────┬──────────────────────┬───────────────────────┤
│   Layer 1     │       Layer 2        │       Layer 3         │
│   Temporal    │   Decay-Weighted     │   STL Knowledge       │
│   Embedding   │     Retrieval        │     Verification      │
├───────────────┼──────────────────────┼───────────────────────┤
│ TMRL subspace │  w = q·c·exp(-β·Δt) │  φ = G[0,n](v ≥ γ)   │
│ preserves age │  epistemic filter    │  weakest-link bound   │
│ at all scales │  τ-threshold pruning │  re-acquisition alert │
└───────────────┴──────────────────────┴───────────────────────┘
         ┌──────────────────────────────────────────┐
         │   chronofy[sl] — Subjective Logic Ext.   │
         │   Full (b,d,u,a) opinion tracking         │
         │   Trust-discounted decay · Conflict       │
         │   detection · Opinion-aware STL & scoring │
         └──────────────────────────────────────────┘

Each layer is independently usable — you do not need all three.


Quick Start

from chronofy import (
    TemporalFact,
    ExponentialDecay,
    EpistemicFilter,
    STLVerifier,
    ReasoningStep,
    ReasoningTrace,
)
from datetime import datetime, timedelta

now = datetime.now()

# 1. Define timestamped facts
facts = [
    TemporalFact(
        content="Serum potassium: 4.1 mEq/L",
        timestamp=now - timedelta(hours=1),
        fact_type="vital_sign",
        source_quality=0.95,
    ),
    TemporalFact(
        content="Serum potassium: 3.2 mEq/L",
        timestamp=now - timedelta(days=180),
        fact_type="vital_sign",
        source_quality=0.95,
    ),
    TemporalFact(
        content="Blood type: O+",
        timestamp=now - timedelta(days=3650),
        fact_type="demographic",
        source_quality=1.0,
    ),
]

# 2. Configure decay — β = 2κ (twice the latent process mean-reversion rate)
decay = ExponentialDecay(
    beta={
        "vital_sign":       5.0,   # volatile — decays in hours/days
        "lab_result":       2.0,   # decays in days/weeks
        "medication":       1.0,   # decays in weeks
        "chronic_condition":0.01,  # stable over months/years
        "demographic":      0.0,   # invariant — never decays
    }
)

# 3. Filter to temporally valid facts only
ep_filter = EpistemicFilter(decay_fn=decay, threshold=0.1)
valid_facts = ep_filter.filter(facts, query_time=now)

for fact in valid_facts:
    score = decay.compute(fact, now)
    print(f"[{score:.4f}] {fact.content}")
# (6-month-old reading filtered out)

# 4. Verify reasoning chain with STL
step = ReasoningStep(step_index=0, content="assess arrhythmia risk", facts_used=valid_facts)
trace = ReasoningTrace(steps=[step], query_time=now)

verifier = STLVerifier(decay_fn=decay, threshold=0.05)
result = verifier.verify(trace)

print(f"STL satisfied:      {result.satisfied}")
print(f"Robustness score:   {result.robustness:.4f}")
print(f"Confidence bound:   {result.output_confidence_bound:.4f}")

if not result.satisfied:
    print(f"Re-acquisition needed for: {result.weakest_fact.content}")
[1.0000] Blood type: O+
[0.7713] Serum potassium: 4.1 mEq/L
STL satisfied:      True
Robustness score:   0.7213
Confidence bound:   0.7713

Core Concepts

TemporalFact — the evidence tuple

Every piece of evidence is represented as a tuple e = (c, t_e, q, m):

from chronofy import TemporalFact

fact = TemporalFact(
    content="Patient serum potassium: 4.1 mEq/L",   # c: the claim
    timestamp=datetime(2024, 6, 1, 8, 30),           # t_e: observation time
    fact_type="lab_result",                           # determines decay rate β_j
    source_quality=0.95,                              # q ∈ (0, 1]: reliability weight
    publication_timestamp=datetime(2024, 6, 1, 9),   # optional: reporting provenance
    source="EPIC/LAB",                               # optional: provenance
    metadata={"units": "mEq/L", "method": "ISE"},   # optional: extra context
)

# Age in days at any reference time
age = fact.age_at(datetime.now())

age_at() and the built-in decay functions compute age from timestamp. The optional publication_timestamp records reporting/publication provenance; it does not change age or validity. For a paper published today about data collected three years ago, set timestamp to the data-collection time and publication_timestamp to today.


Decay Functions

All decay functions implement the DecayFunction protocol and return a validity score V(e, T_q) ∈ [0, 1] — incorporating both temporal decay and source reliability.

ExponentialDecay (recommended default)

Grounded in Bayesian decision theory: for a latent state following an Ornstein-Uhlenbeck process with mean-reversion rate κ, the information-theoretically optimal decay rate is β = 2κ (Proposition 1, Chronofy paper).

from chronofy import ExponentialDecay

decay = ExponentialDecay(
    beta={"vital_sign": 5.0, "demographic": 0.0},
    default_beta=0.5,
    time_unit="days",       # "days" | "hours" | "seconds"
)

# Construct directly from mean-reversion rates (β = 2κ)
decay = ExponentialDecay.from_mean_reversion_rate(
    kappa={"vital_sign": 2.5, "lab_result": 1.0, "demographic": 0.0}
)

# Inspect half-life
print(decay.half_life("vital_sign"))   # ln(2)/5.0 ≈ 0.139 days ≈ 3.3 hours
print(decay.half_life("demographic"))  # None (invariant)

Other built-in backends

from chronofy import HalfLifeDecay, LinearDecay, PowerLawDecay, WeibullDecay

# HalfLifeDecay — same as exponential, parameterised by half-life in days
decay = HalfLifeDecay(half_life={"vital_sign": 3.0, "lab_result": 14.0})

# LinearDecay — hard expiry at 1/α days; useful for compliance deadlines
decay = LinearDecay(rate={"regulatory": 0.1}, default_rate=0.05)

# PowerLawDecay — heavy-tailed decay; matches citation/web freshness patterns
decay = PowerLawDecay(exponent={"news": 1.5})

# WeibullDecay — generalized exponential from survival analysis
# shape > 1: accelerating decay; shape < 1: decelerating decay
decay = WeibullDecay(scale={"lab_result": 7.0}, shape={"lab_result": 1.5})

EpistemicFilter — Layer 2 retrieval gate

Structurally excludes stale evidence from the context window before it reaches the LLM. Facts below the validity threshold τ are pruned.

from chronofy import EpistemicFilter, ExponentialDecay

decay = ExponentialDecay(beta={"general": 1.0})
ep_filter = EpistemicFilter(decay_fn=decay, threshold=0.1)

query_time = datetime.now()
valid_facts = ep_filter.filter(facts, query_time)

# Partition into valid/stale
fresh, stale = ep_filter.partition(facts, query_time)

# Check if any fact type needs re-acquisition
needs_refresh = ep_filter.needs_reacquisition(facts, query_time)

Property (Retrieval Completeness): Under threshold τ, the retrieved context excludes all facts e satisfying q_e · c(tr_j) · exp(-β_j · Δt) < τ, guaranteeing the LLM never receives evidence below the minimum validity floor.


STLVerifier — Layer 3 reasoning guard

Applies Signal Temporal Logic robustness to the temporal validity of retrieved knowledge at each reasoning step — not to the LLM's output confidence. This is the headline novelty: the LLM can be confidently wrong when it retrieves plausible but stale facts. STL over knowledge validity catches this.

STL specification: φ_valid = G[0,n]( v(s_i) ≥ γ )

where v(s_i) = min over facts used at step i of V(e, T_q).

Robustness score: ρ = min_i (v(s_i) − γ)

  • ρ ≥ 0: reasoning satisfies the temporal validity constraint
  • ρ < 0: at least one step relies on insufficiently fresh evidence
from chronofy import STLVerifier, STLResult, ReasoningStep, ReasoningTrace

verifier = STLVerifier(decay_fn=decay, threshold=0.3)

step1 = ReasoningStep(step_index=0, content="retrieve labs", facts_used=[lab_fact])
step2 = ReasoningStep(step_index=1, content="assess risk",   facts_used=[lab_fact, dx_fact])
trace = ReasoningTrace(steps=[step1, step2], query_time=datetime.now())

result = verifier.verify(trace)

print(f"ρ = {result.robustness:.4f}")           # scalar robustness
print(f"Satisfied: {result.satisfied}")          # ρ ≥ 0
print(f"Step validity: {result.step_validity}")  # per-step signals
print(f"C_out ≤ {result.output_confidence_bound:.4f}")  # Theorem 1 bound
print(f"Weakest fact: {result.weakest_fact.content}")

# Re-acquisition trigger
if not result.satisfied:
    print("Insufficient temporal context — data re-acquisition required.")

Theorem 1 (Weakest-Link Bound): For any reasoning chain using facts {e_1, ..., e_k}, the maximum reliable output confidence is bounded by min_i V(e_i, T_q). This follows from the min-aggregation principle of possibilistic logic.


CorpusStats — temporal health analysis

Statistical summary of a fact corpus at a given query time. Useful for understanding your data before any RAG run.

from chronofy import CorpusStats, ExponentialDecay

decay = ExponentialDecay(beta={"lab_result": 2.0, "vital_sign": 5.0})
stats = CorpusStats(facts=my_facts, query_time=datetime.now(), decay_fn=decay)

# Age distribution (in days)
print(stats.age_stats.mean)    # mean age
print(stats.age_stats.p75)     # 75th percentile age
print(stats.temporal_span_days)

# Validity distribution
print(stats.validity_stats.min)
print(stats.effective_density)          # mean validity — single-number health score

# Staleness
print(stats.staleness_rate(threshold=0.1))   # fraction of facts below 10% validity

# Silent periods
gaps = stats.coverage_gaps(min_gap_days=7)   # gaps with no facts for ≥ 7 days
for start, end in gaps:
    print(f"  Gap: {start.date()}{end.date()}")

# Full summary dict (JSON-serialisable)
summary = stats.summary()

BetaEstimator — fit β from data

When you do not know the latent process dynamics analytically, fit β empirically from labeled observations (age_in_days, still_valid: bool).

from chronofy import BetaEstimator, MLEBernoulli, MomentMatching, EnsembleMethod

# Default: MLE under Bernoulli model P(valid | age, β) = exp(-β · age)
estimator = BetaEstimator()

result = estimator.fit(
    ages=[1.0, 5.0, 10.0, 30.0, 60.0],
    valid=[True,  True,  True,  False, False],
    fact_type="lab_result",
)
print(f"β̂ = {result.beta:.4f}")
print(f"Half-life = {result.half_life:.1f} days")
print(f"Log-likelihood = {result.log_likelihood:.4f}")

# Fit per fact_type from a labelled corpus
results = estimator.fit_corpus(
    facts=my_facts,
    labels=my_labels,      # parallel list of bools
    query_time=datetime.now(),
)
for fact_type, r in results.items():
    print(f"{fact_type:20s}  β={r.beta:.4f}  t½={r.half_life:.1f}d")

# Convert directly to a ready-to-use ExponentialDecay
decay = estimator.to_decay(results, default_beta=0.5)
score = decay.compute(my_fact, datetime.now())

Swapping the estimation backend

# Method of moments — closed-form, faster, slightly less efficient
estimator = BetaEstimator(method=MomentMatching())

# Ensemble — weighted combination of any methods
estimator = BetaEstimator(
    method=EnsembleMethod(
        methods=[MLEBernoulli(), MomentMatching()],
        weights=[0.7, 0.3],
    )
)

Custom Extensions

Chronofy is designed to be extended. Both the decay backend and the estimation backend are swappable via abstract base classes.

Custom Decay Function

Subclass DecayFunction and implement two methods:

import math
from datetime import datetime
from chronofy import DecayFunction, TemporalFact, validate_decay_function

class SeasonalDecay(DecayFunction):
    """Decay that accounts for seasonal recurrence patterns.

    Information that repeats on a yearly cycle (e.g. seasonal disease
    prevalence, annual financial reports) becomes relevant again after
    ~365 days rather than continuing to decay monotonically.
    """

    def __init__(self, base_rate: float = 0.5, period_days: float = 365.0):
        self.base_rate = base_rate
        self.period_days = period_days

    def compute(self, fact: TemporalFact, query_time: datetime) -> float:
        age_days = fact.age_at(query_time)
        # Monotone decay modulated by seasonal cosine
        monotone = math.exp(-self.base_rate * age_days)
        seasonal = 0.5 * (1 + math.cos(2 * math.pi * age_days / self.period_days))
        return fact.source_quality * monotone * seasonal

    def compute_batch(self, facts: list[TemporalFact], query_time: datetime) -> list[float]:
        return [self.compute(f, query_time) for f in facts]

# Validate before use — catches bugs immediately with clear error messages
decay = validate_decay_function(SeasonalDecay(base_rate=0.01, period_days=365))

# Use anywhere a DecayFunction is accepted
from chronofy import EpistemicFilter, STLVerifier, CorpusStats
ep_filter  = EpistemicFilter(decay_fn=decay, threshold=0.05)
verifier   = STLVerifier(decay_fn=decay, threshold=0.1)
stats      = CorpusStats(facts=my_facts, query_time=datetime.now(), decay_fn=decay)

Custom Estimation Method

Subclass EstimationMethod and implement one method:

import math
from chronofy import EstimationMethod, BetaEstimator, validate_estimation_method

class MedianHalfLifeEstimator(EstimationMethod):
    """Estimates β by finding the age at which ~50% of facts become invalid.

    A non-parametric alternative: sort facts by age, find the median
    age among invalid ones, then set β = ln(2) / median_invalid_age.
    Robust to outliers and requires no optimisation.
    """

    def fit(self, ages: list[float], valid: list[bool]) -> float:
        invalid_ages = sorted(a for a, v in zip(ages, valid) if not v)
        if not invalid_ages:
            return 0.0   # all valid → effectively no decay
        median_invalid_age = invalid_ages[len(invalid_ages) // 2]
        if median_invalid_age <= 0:
            return 50.0  # invalid at age 0 → very fast decay
        return math.log(2) / median_invalid_age

# Validate before use
method = validate_estimation_method(MedianHalfLifeEstimator())

# Plug into BetaEstimator
estimator = BetaEstimator(method=method)
result = estimator.fit(ages=[1.0, 5.0, 10.0, 30.0], valid=[True, True, False, False])

# Or combine with other methods in an ensemble
from chronofy import EnsembleMethod, MLEBernoulli
ensemble = EnsembleMethod(
    methods=[MLEBernoulli(), method],
    weights=[0.6, 0.4],
)
estimator = BetaEstimator(method=ensemble)

Plugin Validation

validate_decay_function and validate_estimation_method run behavioural smoke-tests and raise PluginValidationError with a clear message if anything is wrong:

from chronofy import validate_decay_function, validate_estimation_method, PluginValidationError

try:
    decay = validate_decay_function(MyCustomDecay())
except PluginValidationError as e:
    print(f"Fix your decay function: {e}")

# What gets checked for decay functions:
#   ✓ Is an instance of DecayFunction
#   ✓ compute() is callable and returns a float
#   ✓ All returned scores are finite (no NaN/inf)
#   ✓ All returned scores are in [0.0, 1.0]
#   ✓ compute_batch() returns a list of the same length as the input

# What gets checked for estimation methods:
#   ✓ Is an instance of EstimationMethod
#   ✓ fit() is callable and returns a float
#   ✓ Returned β is finite (no NaN/inf)
#   ✓ Returned β ≥ 0 (non-negative by definition)

Decision-Theoretic Grounding

The decay coefficient β is not an arbitrary hyperparameter. Under Gaussian latent dynamics (Ornstein-Uhlenbeck process dθ = -κ(θ-μ)dt + σdW), the information content of a measurement at age Δt decays as:

I(t_e → t) ∝ exp(-2κ · Δt)

Therefore the optimal decay coefficient is β = 2κ — twice the mean-reversion rate of the underlying latent process. This means:

  • β_vital_sign ≈ 5.0 → κ ≈ 2.5 day⁻¹ (physiology reverts in hours)
  • β_lab_result ≈ 2.0 → κ ≈ 1.0 day⁻¹ (labs shift in days)
  • β_demographic = 0.0 → κ = 0 (blood type is invariant)

Temporal Invariance Guarantee: When β = 0, exp(-β·Δt) = 1 for all ages. Age therefore adds no penalty: validity remains the fact's source-quality weight q, regardless of how old the fact is.

See the Chronofy arXiv preprint for the full derivation.


Full Pipeline

ChronofyPipeline orchestrates Layers 2–3 over candidate facts that have already been retrieved. It does not run Layer 1 temporal embedding or retrieval.

The direct constructor accepts pre-built components as ChronofyPipeline(filter=epistemic_filter, verifier=stl_verifier). The default() factory instead creates an ExponentialDecay shared by both components and accepts beta, default_beta, time_unit, filter_threshold, and verifier_threshold.

process(candidate_facts, query_time, build_trace) filters the candidates, passes the surviving list[TemporalFact] to build_trace, and verifies the callback's ReasoningTrace. It returns (valid_facts, stl_result). When no facts survive, the callback is not called and stl_result is None. The trace's own query_time drives verification, so the callback should set it to the same reference time when that is the intended behavior.

from datetime import datetime, timedelta
from chronofy import ChronofyPipeline, ReasoningStep, ReasoningTrace, TemporalFact

query_time = datetime(2026, 3, 15, 12, 0)
candidate_facts = [
    TemporalFact(
        content="Current potassium: 4.1 mEq/L",
        timestamp=query_time - timedelta(hours=1),
        fact_type="vital_sign",
    ),
    TemporalFact(
        content="Old potassium: 3.2 mEq/L",
        timestamp=query_time - timedelta(days=180),
        fact_type="vital_sign",
    ),
    TemporalFact(
        content="Blood type: O+",
        timestamp=query_time - timedelta(days=3650),
        fact_type="demographic",
    ),
]

pipeline = ChronofyPipeline.default(
    beta={"vital_sign": 5.0, "demographic": 0.0},
    filter_threshold=0.1,     # τ: epistemic filter threshold
    verifier_threshold=0.3,   # γ: STL validity threshold
)


def build_trace(valid_facts: list[TemporalFact]) -> ReasoningTrace:
    return ReasoningTrace(
        steps=[
            ReasoningStep(
                step_index=0,
                content="Assess arrhythmia risk",
                facts_used=valid_facts,
            )
        ],
        query_time=query_time,
    )

valid_facts, stl_result = pipeline.process(
    candidate_facts=candidate_facts,
    query_time=query_time,
    build_trace=build_trace,
)

print([fact.content for fact in valid_facts])
if stl_result is None:
    print("No facts survived; re-acquisition is needed.")
else:
    print(f"STL satisfied:    {stl_result.satisfied}")
    print(f"Robustness:       {stl_result.robustness:.4f}")
    print(f"Confidence bound: {stl_result.output_confidence_bound:.4f}")
['Blood type: O+', 'Current potassium: 4.1 mEq/L']
STL satisfied:    True
Robustness:       0.5119
Confidence bound: 0.8119

API Reference

Models

Class Description
TemporalFact Evidence tuple (c, t_e, q, m) with .age_at(query_time)
ReasoningStep One step in a CoT trace with associated facts
ReasoningTrace Full chain-of-thought trace over a query time

Decay Backends

Class Parameterisation Best for
ExponentialDecay beta per fact type General default; OU-optimal
HalfLifeDecay half_life in days Intuitive configuration
LinearDecay rate (validity/day) Hard compliance deadlines
PowerLawDecay exponent Citations, web freshness
WeibullDecay scale, shape Survival analysis domains
DecayFunction ABC Custom implementations

Embedding (Layer 1)

Class Description
TemporalEncoder ABC for temporal encoding
SinusoidalEncoder Fourier-based temporal projection
TemporalEmbedder [e_temp ; e_sem] concatenation
LearnedEncoder Trainable nn.Module temporal encoder
TemporalFineTuner LoRA injection + CKA contrastive training

Retrieval (Layer 2)

Class Description
EpistemicFilter τ-threshold gate; .filter(), .partition(), .needs_reacquisition()
TemporalTriple Timestamped (subject, predicate, object, timestamp) triple
TemporalKnowledgeGraph Directed/undirected temporal knowledge graph with time-range queries
TemporalRule Temporal logical rule with confidence, body/head patterns
RuleMiner Apriori-based level-wise temporal rule mining
TemporalRuleGraph MDL-optimized rule graph with decay-weighted Personalized PageRank

Grounded rule-graph ranking and queries require the source triple store: TemporalRuleGraph(rules, decay_fn, knowledge_graph=tkg). The older known_entities= argument remains an optional allowlist, but entity names alone cannot ground rules or facts. If knowledge_graph is omitted, PageRank and edge weights safely resolve to zero and query() returns no results; it no longer constructs representative facts from rule statistics.

Scoring

Class Description
TemporalScorer Scores facts by strategy(similarity, validity)
ScoredFact Fact + similarity + validity + combined_score
MultiplicativeScoring sim × val (default)
HarmonicScoring 2·sim·val / (sim + val)
WeightedBlendScoring(α) α·sim + (1-α)·val
PowerScoring(α) sim^α · val^(1-α)

Verification (Layer 3)

Class Description
STLVerifier STL robustness over knowledge validity; .verify(), .robustness()
STLResult Robustness score, step validity, weakest fact, confidence bound

Analysis

Class Description
CorpusStats Age/validity distribution, staleness rate, coverage gaps, effective density
AgeStats Frozen dataclass: min/max/mean/median/std/p25/p75 of ages
ValidityStats Frozen dataclass: min/max/mean/median/std/p25/p75 of validity scores
BetaEstimator Fits β from (age, valid) observations; .fit(), .fit_corpus(), .to_decay()
DecayComparison Evaluates multiple decay backends on labelled data; .compare(), .best(), .summary()
DecayComparisonResult Frozen dataclass: name, auc_roc, brier_score, log_loss, ranking_correlation
BetaEstimateResult β, half-life, log-likelihood, n_obs, n_valid, converged

Estimation Methods

Class Description
MLEBernoulli MLE under P(valid|age,β) = exp(-β·age); golden-section search
MomentMatching Closed-form: β = -log(p_valid) / mean_age
EnsembleMethod Normalised weighted average of any EstimationMethod instances
EstimationMethod ABC — subclass to add any estimation technique

Plugin Utilities

Symbol Description
validate_decay_function(fn) Behavioural smoke-test; returns fn on success
validate_estimation_method(m) Behavioural smoke-test; returns m on success
PluginValidationError Raised with a descriptive message on validation failure

Subjective Logic Extension (chronofy[sl])

The core framework uses scalar validity scores V ∈ [0, 1]. The chronofy[sl] extension enriches this with Jøsang's Subjective Logic Opinions ω = (b, d, u, a), which distinguish evidential direction (belief vs disbelief) from epistemic uncertainty — a distinction that scalars conflate.

pip install chronofy[sl]

Requires jsonld-ex and NetworkX (automatically installed).

Why Opinions matter

A scalar validity of 0.5 could mean:

  • "We have strong evidence that the probability is 50%" (b=0.45, d=0.45, u=0.10)
  • "We have no evidence at all" (b=0.0, d=0.0, u=1.0, a=0.5)

These require very different downstream behavior. The SL extension carries this distinction through its opinion-aware filtering, retrieval/scoring, fusion/conflict diagnostics, and STL verification components.

Opinion-Based Decay

from chronofy.sl import OpinionDecayFunction, OpinionConfig

# Configure per fact type: half-life in days, base rate, base uncertainty
odf = OpinionDecayFunction(
    half_lives={"vital_sign": 1.0, "chronic": 69.3, "demographic": 36500.0},
    base_rates={"demographic": 0.9},
    construction="confidence",   # or "evidence" for count-based sources
    base_uncertainty=0.1,        # irreducible epistemic uncertainty
)

# Scalar compatibility — works with EpistemicFilter, STLVerifier, TemporalRuleGraph
score = odf.compute(fact, query_time)           # returns P(ω') ∈ [0, 1]

# Full opinion — for SL-aware consumers
opinion = odf.compute_opinion(fact, query_time)  # returns Opinion(b, d, u, a)
print(f"b={opinion.belief:.3f}, u={opinion.uncertainty:.3f}")
# A 6-month-old potassium reading: belief has decayed, uncertainty is high,
# but the b/d ratio is preserved — it still tells you the value was LOW.

Trust-Discounted Decay

Model source reliability as a full Opinion, not a scalar multiplier. Absorbs ASEV Axioms 3-4 (reliability/Blackwell monotonicity).

from chronofy.sl import TrustProfile, TrustWeightedDecay
from datetime import datetime, timedelta

now = datetime.now()

# Trust opinions per source, with optional temporal decay on the trust itself
tp = TrustProfile.from_scalars(
    {"hospital_lab": 0.95, "patient_report": 0.60, "wearable": 0.70},
    uncertainty=0.05,
    timestamps={"hospital_lab": now - timedelta(days=365)},  # certified 1yr ago
    half_lives={"hospital_lab": 730.0},  # trust half-life: 2 years
)

# Two independent decay channels:
#   ω_Ax = decay(ω_trust, age_of_trust) ⊗ decay(ω_evidence, age_of_evidence)
twd = TrustWeightedDecay(decay_fn=odf, trust_profile=tp)
opinion = twd.compute_opinion(fact, query_time=now)

Evidence Fusion

Combine multiple observations of the same proposition with diminishing returns (ASEV Axiom 6).

from chronofy.sl import TemporalEvidenceFusion

fuser = TemporalEvidenceFusion(
    decay_fn=odf,
    fusion_method="cumulative",  # independent sources (or "averaging" for correlated)
    byzantine=True,              # remove adversarial outliers before fusion
    byzantine_threshold=0.15,
)

report = fuser.fuse(potassium_readings, query_time=now)
print(report.fused_opinion)          # combined (b, d, u, a)
print(report.projected_probability)  # scalar fallback
print(report.removed_count)          # Byzantine-filtered outliers

Conflict Detection

Detect contradictory evidence before fusion or LLM consumption.

from chronofy.sl import ConflictDetector

detector = ConflictDetector(decay_fn=odf, default_threshold=0.15)
report = detector.detect(facts, query_time=now)

print(f"Cohesion: {report.cohesion_score:.3f}")  # 1.0 = perfect agreement
for i, j, score in report.conflict_pairs:
    print(f"  Facts {i} vs {j}: conflict = {score:.3f}")

# Also available for pre-decayed opinions (e.g., from trust pipeline)
report = ConflictDetector.detect_from_opinions(opinions, threshold=0.2)

Opinion-Aware STL Verification

Richer weakest-link bound that distinguishes stale evidence (high u) from negative evidence (high d).

from chronofy.sl import OpinionSTLVerifier

verifier = OpinionSTLVerifier(decay_fn=odf, threshold=0.6)
result = verifier.verify(trace)

print(f"ρ = {result.robustness:.4f}")
print(f"Weakest link: u={result.weakest_link_opinion.uncertainty:.3f}")

# Re-acquisition diagnostics
if not result.satisfied:
    wl = result.weakest_link_opinion
    if wl.uncertainty > 0.8:
        print("Stale evidence — re-acquire same source type")
    elif wl.disbelief > wl.belief:
        print("Negative evidence — real finding, not staleness")
    else:
        print("Weak evidence — need additional sources")

Opinion-Aware Scoring

Scoring strategies that can inspect uncertainty directly.

from chronofy.sl import OpinionScorer, UncertaintyPenalized, UncertaintyAwareBlend

# UncertaintyPenalized: sim × P(ω) × (1-u)
# Suppresses uncertain (stale) evidence more aggressively than scalar scoring
scorer = OpinionScorer(decay_fn=odf, strategy=UncertaintyPenalized())
ranked = scorer.rank(facts, similarities, query_time=now, top_k=5)

for sf in ranked:
    print(f"[{sf.combined_score:.3f}] u={sf.validity_opinion.uncertainty:.2f}  {sf.fact.content}")

# Three-way blend: α·sim + β·P(ω) + γ·(1-u)
scorer = OpinionScorer(
    decay_fn=odf,
    strategy=UncertaintyAwareBlend(alpha=0.4, beta=0.4),  # γ=0.2 for certainty
)

SL Module Summary

Symbol Description
Opinion Subjective Logic opinion (belief, disbelief, uncertainty, base rate) from jsonld-ex
OpinionDecayFunction compute() returns projected probability; compute_opinion() returns the full Opinion
OpinionConfig Per-fact-type half_life, base_rate, base_uncertainty
TemporalEvidenceFusion Decay → (Byzantine filter) → cumulative/averaging fusion
FusionReport Fused opinion, per-source opinions, removed indices
ConflictDetector Pairwise conflict matrix, discord scores, cohesion
ConflictReport Full diagnostics: matrix, pairs, discord, internal conflicts
TrustEntry A source-trust Opinion with optional timestamp and half-life
TrustProfile Source → trust Opinion mapping with temporal trust decay
TrustWeightedDecay Dual decay channels: evidence aging ⊗ trust aging
OpinionSTLVerifier STL verification with per-step Opinions
OpinionSTLResult Step opinions, weakest-link opinion, scalar compat
OpinionScorer Rank facts using Opinion-aware strategies
OpinionScoredFact Fact + Opinion + scalar validity + combined score
OpinionScoringStrategy Abstract base for Opinion-aware scoring strategies
ProjectedMultiplicative sim × P(ω) — scalar-equivalent default
UncertaintyPenalized sim × P(ω) × (1-u) — penalizes uncertain evidence
UncertaintyAwareBlend α·sim + β·P(ω) + γ·(1-u) — three-way blend
SLPipeline Layer 2 filtering and Opinion-aware Layer 3 verification, with optional conflict and grouped fusion
SLPipelineResult Valid facts plus optional STL, conflict, grouped-fusion, and re-acquisition diagnostics
GroupedFusionResult Per-group fusion reports; fusion is performed only within each group
OpinionRuleGraph TemporalRuleGraph wrapper with Opinion-aware queries and optional probability/uncertainty filtering
OpinionEpistemicFilter Projected-probability and uncertainty thresholds with valid/stale/uncertain partitioning
OpinionPartitionResult Valid, stale, and uncertain (fact, Opinion) collections

When SLPipeline fusion is enabled, facts are grouped by fact.metadata["proposition_key"] when present, otherwise by exact content. fact_type is a decay category and is no longer used as the default proposition identity. For repeated observations whose values change, set a stable metadata key or pass fusion_group_by; callers whose domain guarantees one proposition per fact type can preserve the former behavior explicitly with fusion_group_by=lambda fact: fact.fact_type.


Citation

@misc{syed2026chronofytemporallogicaldecayarchitecture,
  title     = {Chronofy: A Temporal-Logical Decay Architecture for Information
               Validity in Time-Aware Retrieval-Augmented Generation},
  author    = {Muntaser Syed and Marius Silaghi and Sheikh Abujar and
               Sharun Akter},
  year      = {2026},
  eprint    = {2607.20560},
  archivePrefix = {arXiv},
  primaryClass  = {cs.LG},
  url       = {https://arxiv.org/abs/2607.20560},
}

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

chronofy-0.1.9-py3-none-any.whl (128.7 kB view details)

Uploaded Python 3

File details

Details for the file chronofy-0.1.9-py3-none-any.whl.

File metadata

  • Download URL: chronofy-0.1.9-py3-none-any.whl
  • Upload date:
  • Size: 128.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.3.2 CPython/3.12.2 Windows/11

File hashes

Hashes for chronofy-0.1.9-py3-none-any.whl
Algorithm Hash digest
SHA256 d45a25ab8813c4fb375986f166b344a4055dd27bac4f198bf2c9ab9e0a5c392e
MD5 836032bed3da7cb5d3399dccde28dc56
BLAKE2b-256 9a1a1eddf0496410bd2c2388a97b0bf01a87e43eaece69cdaa3d583109119b25

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page