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:
start_bin
is the index of the first bin touchedend_bin
is the exclusive upper bound (i.e., first bin not touched)start_value
andend_value
represent fractional presence at the edges- Bins between
start_bin
andend_bin
are fully or partially covered
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.
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_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
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.