pcalc.presence_invariant

 1# -*- coding: utf-8 -*-
 2# Copyright (c) 2025 Krishna Kumar
 3# SPDX-License-Identifier: MIT
 4
 5from typing import Tuple
 6from .basis_topology import BasisTopology
 7from .presence import PresenceAssertion
 8
 9
10class PresenceInvariant:
11    """
12    Computes the components of the Presence Invariant directly from a BasisTopology.
13
14    This invariant expresses a local conservation law of presence mass over any finite
15    time interval [t0, t1), using only the closure of the topology.
16    """
17
18    def __init__(self, topology: BasisTopology):
19        self.topology = topology
20        self.closed_presences = topology.closure()
21
22    def _filter_window(self, t0: float, t1: float) -> list[PresenceAssertion]:
23        return [
24            p for p in self.closed_presences
25            if p.overlaps(t0,t1)
26        ]
27
28    def get_presence_summary(self, t0: float, t1: float) -> Tuple[float, int, float]:
29        """
30        Computes:
31        - Total presence mass A
32        - Number of overlapping presences N
33        - Interval length T
34        """
35        if t0 >= t1:
36            return 0.0, 0, 0.0
37
38        A = 0.0
39        N = 0
40        T = t1 - t0
41
42        for p in self._filter_window(t0, t1):
43            r = p.mass_contribution(t0, t1)
44            if r > 0.0:
45                A += r
46                N += 1
47
48        return A, N, T
49
50    def avg_presence_density(self, t0: float, t1: float) -> float:
51        A, _, T = self.get_presence_summary(t0, t1)
52        return A / T if T > 0 else 0.0
53
54    def incidence_rate(self, t0: float, t1: float) -> float:
55        _, N, T = self.get_presence_summary(t0, t1)
56        return N / T if T > 0 else 0.0
57
58
59    def avg_presence_mass(self, t0: float, t1: float) -> float:
60        A, N, _ = self.get_presence_summary(t0, t1)
61        return A / N if N > 0 else 0.0
62
63    def invariant(self, t0: float, t1: float) -> Tuple[float, float, float]:
64        """
65        Returns:
66            (avg_presence_density, incidence_rate, avg_presence_mass)
67
68        These satisfy the Presence Invariant:
69            avg_presence_density = incidence_rate × avg_presence_mass
70        """
71        A, N, T = self.get_presence_summary(t0, t1)
72        L = A / T if T > 0 else 0.0
73        Λ = N / T if T > 0 else 0.0
74        w = A / N if N > 0 else 0.0
75        return L, Λ, w
class PresenceInvariant:
11class PresenceInvariant:
12    """
13    Computes the components of the Presence Invariant directly from a BasisTopology.
14
15    This invariant expresses a local conservation law of presence mass over any finite
16    time interval [t0, t1), using only the closure of the topology.
17    """
18
19    def __init__(self, topology: BasisTopology):
20        self.topology = topology
21        self.closed_presences = topology.closure()
22
23    def _filter_window(self, t0: float, t1: float) -> list[PresenceAssertion]:
24        return [
25            p for p in self.closed_presences
26            if p.overlaps(t0,t1)
27        ]
28
29    def get_presence_summary(self, t0: float, t1: float) -> Tuple[float, int, float]:
30        """
31        Computes:
32        - Total presence mass A
33        - Number of overlapping presences N
34        - Interval length T
35        """
36        if t0 >= t1:
37            return 0.0, 0, 0.0
38
39        A = 0.0
40        N = 0
41        T = t1 - t0
42
43        for p in self._filter_window(t0, t1):
44            r = p.mass_contribution(t0, t1)
45            if r > 0.0:
46                A += r
47                N += 1
48
49        return A, N, T
50
51    def avg_presence_density(self, t0: float, t1: float) -> float:
52        A, _, T = self.get_presence_summary(t0, t1)
53        return A / T if T > 0 else 0.0
54
55    def incidence_rate(self, t0: float, t1: float) -> float:
56        _, N, T = self.get_presence_summary(t0, t1)
57        return N / T if T > 0 else 0.0
58
59
60    def avg_presence_mass(self, t0: float, t1: float) -> float:
61        A, N, _ = self.get_presence_summary(t0, t1)
62        return A / N if N > 0 else 0.0
63
64    def invariant(self, t0: float, t1: float) -> Tuple[float, float, float]:
65        """
66        Returns:
67            (avg_presence_density, incidence_rate, avg_presence_mass)
68
69        These satisfy the Presence Invariant:
70            avg_presence_density = incidence_rate × avg_presence_mass
71        """
72        A, N, T = self.get_presence_summary(t0, t1)
73        L = A / T if T > 0 else 0.0
74        Λ = N / T if T > 0 else 0.0
75        w = A / N if N > 0 else 0.0
76        return L, Λ, w

Computes the components of the Presence Invariant directly from a BasisTopology.

This invariant expresses a local conservation law of presence mass over any finite time interval [t0, t1), using only the closure of the topology.

PresenceInvariant(topology: pcalc.BasisTopology)
19    def __init__(self, topology: BasisTopology):
20        self.topology = topology
21        self.closed_presences = topology.closure()
topology
closed_presences
def get_presence_summary(self, t0: float, t1: float) -> Tuple[float, int, float]:
29    def get_presence_summary(self, t0: float, t1: float) -> Tuple[float, int, float]:
30        """
31        Computes:
32        - Total presence mass A
33        - Number of overlapping presences N
34        - Interval length T
35        """
36        if t0 >= t1:
37            return 0.0, 0, 0.0
38
39        A = 0.0
40        N = 0
41        T = t1 - t0
42
43        for p in self._filter_window(t0, t1):
44            r = p.mass_contribution(t0, t1)
45            if r > 0.0:
46                A += r
47                N += 1
48
49        return A, N, T

Computes:

  • Total presence mass A
  • Number of overlapping presences N
  • Interval length T
def avg_presence_density(self, t0: float, t1: float) -> float:
51    def avg_presence_density(self, t0: float, t1: float) -> float:
52        A, _, T = self.get_presence_summary(t0, t1)
53        return A / T if T > 0 else 0.0
def incidence_rate(self, t0: float, t1: float) -> float:
55    def incidence_rate(self, t0: float, t1: float) -> float:
56        _, N, T = self.get_presence_summary(t0, t1)
57        return N / T if T > 0 else 0.0
def avg_presence_mass(self, t0: float, t1: float) -> float:
60    def avg_presence_mass(self, t0: float, t1: float) -> float:
61        A, N, _ = self.get_presence_summary(t0, t1)
62        return A / N if N > 0 else 0.0
def invariant(self, t0: float, t1: float) -> Tuple[float, float, float]:
64    def invariant(self, t0: float, t1: float) -> Tuple[float, float, float]:
65        """
66        Returns:
67            (avg_presence_density, incidence_rate, avg_presence_mass)
68
69        These satisfy the Presence Invariant:
70            avg_presence_density = incidence_rate × avg_presence_mass
71        """
72        A, N, T = self.get_presence_summary(t0, t1)
73        L = A / T if T > 0 else 0.0
74        Λ = N / T if T > 0 else 0.0
75        w = A / N if N > 0 else 0.0
76        return L, Λ, w
Returns:

(avg_presence_density, incidence_rate, avg_presence_mass)

These satisfy the Presence Invariant:

avg_presence_density = incidence_rate × avg_presence_mass