pcalc.presence_map

  1# -*- coding: utf-8 -*-
  2# Copyright (c) 2025 Krishna Kumar
  3# SPDX-License-Identifier: MIT
  4
  5
  6from __future__ import annotations
  7
  8from dataclasses import dataclass
  9
 10from .presence import PresenceAssertion
 11from .time_scale import Timescale
 12
 13
 14@dataclass
 15class PresenceMap:
 16    """
 17        A PresenceMap maps a continuous presence interval [start, end)
 18        onto a discrete time grid defined by a Timescale.
 19
 20        The result is a bin-aligned representation:
 21        - `start_bin` is the index of the first bin touched
 22        - `end_bin` is the exclusive upper bound (i.e., first bin not touched)
 23        - `start_value` and `end_value` represent fractional presence at the edges
 24        - Bins between `start_bin` and `end_bin` are fully or partially covered
 25
 26        Contract:
 27        - A presence is considered "mapped" if it overlaps the timescale [t0, t1)
 28        - Mapped presences always produce:
 29            start_bin ∈ [0, num_bins)
 30            end_bin   ∈ (start_bin, num_bins]
 31        - The bin range [start_bin, end_bin) contains all and only the bins the presence overlaps
 32        - start_value ∈ (0.0, 1.0] if partially covers `start_bin`
 33        - end_value   ∈ (0.0, 1.0] if partially covers `end_bin - 1`
 34    """
 35
 36    presence: PresenceAssertion
 37    """The presence entry"""
 38    time_scale: Timescale
 39    """The time scale that the presence is mapped to"""
 40
 41    is_mapped: bool
 42    """True if the presence has a valid mapping. Unmapped presences have value=-1
 43    for start_slice, end_slice, start_value and end_value. 
 44    """
 45
 46    start_bin: int
 47    """The starting bin in the discrete mapping"""
 48    end_bin: int
 49    """The ending bin in the discrete mapping` 
 50    of row `row`. 
 51    """
 52    start_value: float
 53    """A presence value 0 < p < 1.0 that represents a potentially partial presence at the start of the mapping"""
 54    end_value: float
 55    """A presence value 0 < p < 1.0 that represents a potentially partial presence at the end of the mapping"""
 56
 57    @property
 58    def bin_range(self) -> range:
 59        return range(self.start_bin, self.end_bin) if self.is_mapped else range(0)
 60
 61    @property
 62    def duration(self) -> float:
 63        return self.presence.reset_time - self.presence.onset_time
 64
 65
 66    def __init__(self, presence: PresenceAssertion, time_scale: Timescale):
 67        """
 68        Map a presence interval to matrix slice indices and edge fractional values
 69        using the provided Timescale object.
 70         """
 71        self.presence = presence
 72        self.time_scale = time_scale
 73        ts = self.time_scale
 74        is_mapped = False
 75        start_bin = -1
 76        end_bin = -1
 77        start_value = -1.0
 78        end_value = -1.0
 79
 80        if presence.overlaps(ts.t0, ts.t1):
 81            is_mapped = True
 82            start_bin, end_bin = ts.bin_slice(presence.onset_time, presence.reset_time)
 83            start_value, end_value = self._compute_fractional_values(presence.onset_time, start_bin, presence.reset_time, end_bin)
 84
 85        self.is_mapped = is_mapped
 86        self.start_bin = start_bin
 87        self.end_bin = end_bin
 88        self.start_value = start_value
 89        self.end_value = end_value
 90
 91    def _compute_fractional_values(self, start_time: float, start_bin: int, end_time: float, end_bin: int):
 92        ts = self.time_scale
 93        # Compute partial overlap at start bin
 94        start_value = ts.fractional_overlap(start_time, end_time, start_bin)
 95
 96        # Compute partial overlap at end bin, if not same as start
 97        if end_bin - 1 > start_bin:
 98            end_value = ts.fractional_overlap(start_time, end_time, end_bin - 1)
 99        else:
100            end_value = 0.0
101
102        return start_value, end_value
103
104
105    def is_active(self, start_bin: int, end_bin: int):
106        return end_bin > self.start_bin and start_bin < self.end_bin
107
108    def presence_value_in(self, start_time: float, end_time: float) -> float:
109        """
110        Computes total presence value (in time) within [start_time, end_time),
111        using bin-based approximation logic, clipped to the given interval.
112        """
113        ts = self.time_scale
114        bin_width = ts.bin_width
115
116        start_bin, end_bin = ts.bin_slice(start_time, end_time)
117        if self.is_mapped and self.is_active(start_bin, end_bin):
118            #  Clip window to actual presence bounds
119            effective_start = max(self.presence.onset_time, start_time)
120            effective_end = min(self.presence.reset_time, end_time)
121
122            effective_start_bin, effective_end_bin = ts.bin_slice(effective_start, effective_end)
123
124            if start_time == ts.t0 and end_time == ts.t1:
125                start_value = self.start_value
126                end_value = self.end_value
127            else:
128                start_value, end_value = self._compute_fractional_values(effective_start, effective_start_bin, effective_end, effective_end_bin)
129
130            full_bin_value = max(0, effective_end_bin - effective_start_bin - 2)
131
132            fractional_value = start_value + end_value
133
134            return (full_bin_value + fractional_value) * bin_width
135        else:
136            return 0.0
137
138    @property
139    def presence_value(self) -> float:
140        return self.presence_value_in(self.time_scale.t0, self.time_scale.t1)
@dataclass
class PresenceMap:
 15@dataclass
 16class PresenceMap:
 17    """
 18        A PresenceMap maps a continuous presence interval [start, end)
 19        onto a discrete time grid defined by a Timescale.
 20
 21        The result is a bin-aligned representation:
 22        - `start_bin` is the index of the first bin touched
 23        - `end_bin` is the exclusive upper bound (i.e., first bin not touched)
 24        - `start_value` and `end_value` represent fractional presence at the edges
 25        - Bins between `start_bin` and `end_bin` are fully or partially covered
 26
 27        Contract:
 28        - A presence is considered "mapped" if it overlaps the timescale [t0, t1)
 29        - Mapped presences always produce:
 30            start_bin ∈ [0, num_bins)
 31            end_bin   ∈ (start_bin, num_bins]
 32        - The bin range [start_bin, end_bin) contains all and only the bins the presence overlaps
 33        - start_value ∈ (0.0, 1.0] if partially covers `start_bin`
 34        - end_value   ∈ (0.0, 1.0] if partially covers `end_bin - 1`
 35    """
 36
 37    presence: PresenceAssertion
 38    """The presence entry"""
 39    time_scale: Timescale
 40    """The time scale that the presence is mapped to"""
 41
 42    is_mapped: bool
 43    """True if the presence has a valid mapping. Unmapped presences have value=-1
 44    for start_slice, end_slice, start_value and end_value. 
 45    """
 46
 47    start_bin: int
 48    """The starting bin in the discrete mapping"""
 49    end_bin: int
 50    """The ending bin in the discrete mapping` 
 51    of row `row`. 
 52    """
 53    start_value: float
 54    """A presence value 0 < p < 1.0 that represents a potentially partial presence at the start of the mapping"""
 55    end_value: float
 56    """A presence value 0 < p < 1.0 that represents a potentially partial presence at the end of the mapping"""
 57
 58    @property
 59    def bin_range(self) -> range:
 60        return range(self.start_bin, self.end_bin) if self.is_mapped else range(0)
 61
 62    @property
 63    def duration(self) -> float:
 64        return self.presence.reset_time - self.presence.onset_time
 65
 66
 67    def __init__(self, presence: PresenceAssertion, time_scale: Timescale):
 68        """
 69        Map a presence interval to matrix slice indices and edge fractional values
 70        using the provided Timescale object.
 71         """
 72        self.presence = presence
 73        self.time_scale = time_scale
 74        ts = self.time_scale
 75        is_mapped = False
 76        start_bin = -1
 77        end_bin = -1
 78        start_value = -1.0
 79        end_value = -1.0
 80
 81        if presence.overlaps(ts.t0, ts.t1):
 82            is_mapped = True
 83            start_bin, end_bin = ts.bin_slice(presence.onset_time, presence.reset_time)
 84            start_value, end_value = self._compute_fractional_values(presence.onset_time, start_bin, presence.reset_time, end_bin)
 85
 86        self.is_mapped = is_mapped
 87        self.start_bin = start_bin
 88        self.end_bin = end_bin
 89        self.start_value = start_value
 90        self.end_value = end_value
 91
 92    def _compute_fractional_values(self, start_time: float, start_bin: int, end_time: float, end_bin: int):
 93        ts = self.time_scale
 94        # Compute partial overlap at start bin
 95        start_value = ts.fractional_overlap(start_time, end_time, start_bin)
 96
 97        # Compute partial overlap at end bin, if not same as start
 98        if end_bin - 1 > start_bin:
 99            end_value = ts.fractional_overlap(start_time, end_time, end_bin - 1)
100        else:
101            end_value = 0.0
102
103        return start_value, end_value
104
105
106    def is_active(self, start_bin: int, end_bin: int):
107        return end_bin > self.start_bin and start_bin < self.end_bin
108
109    def presence_value_in(self, start_time: float, end_time: float) -> float:
110        """
111        Computes total presence value (in time) within [start_time, end_time),
112        using bin-based approximation logic, clipped to the given interval.
113        """
114        ts = self.time_scale
115        bin_width = ts.bin_width
116
117        start_bin, end_bin = ts.bin_slice(start_time, end_time)
118        if self.is_mapped and self.is_active(start_bin, end_bin):
119            #  Clip window to actual presence bounds
120            effective_start = max(self.presence.onset_time, start_time)
121            effective_end = min(self.presence.reset_time, end_time)
122
123            effective_start_bin, effective_end_bin = ts.bin_slice(effective_start, effective_end)
124
125            if start_time == ts.t0 and end_time == ts.t1:
126                start_value = self.start_value
127                end_value = self.end_value
128            else:
129                start_value, end_value = self._compute_fractional_values(effective_start, effective_start_bin, effective_end, effective_end_bin)
130
131            full_bin_value = max(0, effective_end_bin - effective_start_bin - 2)
132
133            fractional_value = start_value + end_value
134
135            return (full_bin_value + fractional_value) * bin_width
136        else:
137            return 0.0
138
139    @property
140    def presence_value(self) -> float:
141        return self.presence_value_in(self.time_scale.t0, self.time_scale.t1)

A PresenceMap maps a continuous presence interval [start, end) onto a discrete time grid defined by a Timescale.

The result is a bin-aligned representation:

Contract:

  • A presence is considered "mapped" if it overlaps the timescale [t0, t1)
  • Mapped presences always produce: start_bin ∈ [0, num_bins) end_bin ∈ (start_bin, num_bins]
  • The bin range [start_bin, end_bin) contains all and only the bins the presence overlaps
  • start_value ∈ (0.0, 1.0] if partially covers start_bin
  • end_value ∈ (0.0, 1.0] if partially covers end_bin - 1
PresenceMap( presence: pcalc.PresenceAssertion, time_scale: pcalc.Timescale)
67    def __init__(self, presence: PresenceAssertion, time_scale: Timescale):
68        """
69        Map a presence interval to matrix slice indices and edge fractional values
70        using the provided Timescale object.
71         """
72        self.presence = presence
73        self.time_scale = time_scale
74        ts = self.time_scale
75        is_mapped = False
76        start_bin = -1
77        end_bin = -1
78        start_value = -1.0
79        end_value = -1.0
80
81        if presence.overlaps(ts.t0, ts.t1):
82            is_mapped = True
83            start_bin, end_bin = ts.bin_slice(presence.onset_time, presence.reset_time)
84            start_value, end_value = self._compute_fractional_values(presence.onset_time, start_bin, presence.reset_time, end_bin)
85
86        self.is_mapped = is_mapped
87        self.start_bin = start_bin
88        self.end_bin = end_bin
89        self.start_value = start_value
90        self.end_value = end_value

Map a presence interval to matrix slice indices and edge fractional values using the provided Timescale object.

The presence entry

time_scale: pcalc.Timescale

The time scale that the presence is mapped to

is_mapped: bool

True if the presence has a valid mapping. Unmapped presences have value=-1 for start_slice, end_slice, start_value and end_value.

start_bin: int

The starting bin in the discrete mapping

end_bin: int

The ending bin in the discrete mapping of rowrow`.

start_value: float

A presence value 0 < p < 1.0 that represents a potentially partial presence at the start of the mapping

end_value: float

A presence value 0 < p < 1.0 that represents a potentially partial presence at the end of the mapping

bin_range: range
58    @property
59    def bin_range(self) -> range:
60        return range(self.start_bin, self.end_bin) if self.is_mapped else range(0)
duration: float
62    @property
63    def duration(self) -> float:
64        return self.presence.reset_time - self.presence.onset_time
def is_active(self, start_bin: int, end_bin: int):
106    def is_active(self, start_bin: int, end_bin: int):
107        return end_bin > self.start_bin and start_bin < self.end_bin
def presence_value_in(self, start_time: float, end_time: float) -> float:
109    def presence_value_in(self, start_time: float, end_time: float) -> float:
110        """
111        Computes total presence value (in time) within [start_time, end_time),
112        using bin-based approximation logic, clipped to the given interval.
113        """
114        ts = self.time_scale
115        bin_width = ts.bin_width
116
117        start_bin, end_bin = ts.bin_slice(start_time, end_time)
118        if self.is_mapped and self.is_active(start_bin, end_bin):
119            #  Clip window to actual presence bounds
120            effective_start = max(self.presence.onset_time, start_time)
121            effective_end = min(self.presence.reset_time, end_time)
122
123            effective_start_bin, effective_end_bin = ts.bin_slice(effective_start, effective_end)
124
125            if start_time == ts.t0 and end_time == ts.t1:
126                start_value = self.start_value
127                end_value = self.end_value
128            else:
129                start_value, end_value = self._compute_fractional_values(effective_start, effective_start_bin, effective_end, effective_end_bin)
130
131            full_bin_value = max(0, effective_end_bin - effective_start_bin - 2)
132
133            fractional_value = start_value + end_value
134
135            return (full_bin_value + fractional_value) * bin_width
136        else:
137            return 0.0

Computes total presence value (in time) within [start_time, end_time), using bin-based approximation logic, clipped to the given interval.

presence_value: float
139    @property
140    def presence_value(self) -> float:
141        return self.presence_value_in(self.time_scale.t0, self.time_scale.t1)