pcalc.time_model

Introduction

TimeModel is a calendar-aware utility that maps external datetime values to continuous float time relative to a chosen origin and unit of measure.

The presence calculus operates over real time axis, and time is therefore represented as a floating point value in all calculations.

To avoid working with arbitrarily large floating point numbers unnecessarily—which can introduce interpretability or numerical precision issues—it is recommended that you define an explicit origin for your time axis and express all time values relative to this origin.

This model supports both fixed-duration units (e.g., 'seconds', 'minutes', 'days') and calendar-relative units (e.g., 'months', 'quarters', 'years').

TimeModel is most useful when interfacing with external timestamped data sources—such as logs, CRM systems, or real-time feeds—where time is expressed as absolute wall-clock timestamps.

In contrast, when working with synthetic or simulated data (e.g., from a simulation engine), such translation is typically unnecessary, as the simulation clock already provides floating point time relative to the start of the simulation.

Parameters:

  • origin: the reference datetime from which time is measured
  • unit: one of 'seconds', 'minutes', 'hours', 'days', 'months', 'quarters', 'years'

Examples

from datetime import datetime
from pc.entity import Entity
from pc.presence import Presence
from pc.time import TimeModel

# Define a time model using seconds since midnight
time_model = TimeModel(origin=datetime(2025, 5, 1), unit="seconds")

# External datetime timestamps
start_time = datetime(2025, 5, 1, 9, 30)
end_time = datetime(2025, 5, 1, 10, 45)

# Convert to float using the time model
start = time_model.to_float(start_time)  # 34200.0
end = time_model.to_float(end_time)      # 38700.0

# Define domain entities
element = Entity(id="cust-001", name="Alice Chen", metadata={"type": "customer"})
boundary = Entity(id="seg-enterprise", name="Enterprise Segment")

# Create the presence assertion
presence = Presence(
    element=element,
    boundary=boundary,
    start=start,
    end=end,
    provenance="crm"
)

print(presence)
  1# -*- coding: utf-8 -*-
  2# Copyright (c) 2025 Krishna Kumar
  3# SPDX-License-Identifier: MIT
  4"""
  5## Introduction
  6
  7`TimeModel` is a calendar-aware utility that maps external `datetime` values
  8to continuous float time relative to a chosen origin and unit of measure.
  9
 10The presence calculus operates over real time axis, and time is therefore
 11represented as a floating point value in all calculations.
 12
 13To avoid working with arbitrarily large floating point numbers unnecessarily—which can
 14introduce interpretability or numerical precision issues—it is recommended
 15that you define an explicit origin for your time axis and express all time values
 16relative to this origin.
 17
 18This model supports both fixed-duration units (e.g., 'seconds', 'minutes',
 19'days') and calendar-relative units (e.g., 'months', 'quarters', 'years').
 20
 21`TimeModel` is most useful when interfacing with external timestamped data
 22sources—such as logs, CRM systems, or real-time feeds—where time is expressed
 23as absolute wall-clock timestamps.
 24
 25In contrast, when working with synthetic or simulated data (e.g., from a
 26simulation engine), such translation is typically unnecessary, as the
 27simulation clock already provides floating point time relative to the start
 28of the simulation.
 29
 30Parameters:
 31- origin: the reference `datetime` from which time is measured
 32- unit: one of 'seconds', 'minutes', 'hours', 'days', 'months', 'quarters', 'years'
 33
 34### Examples
 35
 36```python
 37from datetime import datetime
 38from pc.entity import Entity
 39from pc.presence import Presence
 40from pc.time import TimeModel
 41
 42# Define a time model using seconds since midnight
 43time_model = TimeModel(origin=datetime(2025, 5, 1), unit="seconds")
 44
 45# External datetime timestamps
 46start_time = datetime(2025, 5, 1, 9, 30)
 47end_time = datetime(2025, 5, 1, 10, 45)
 48
 49# Convert to float using the time model
 50start = time_model.to_float(start_time)  # 34200.0
 51end = time_model.to_float(end_time)      # 38700.0
 52
 53# Define domain entities
 54element = Entity(id="cust-001", name="Alice Chen", metadata={"type": "customer"})
 55boundary = Entity(id="seg-enterprise", name="Enterprise Segment")
 56
 57# Create the presence assertion
 58presence = Presence(
 59    element=element,
 60    boundary=boundary,
 61    start=start,
 62    end=end,
 63    provenance="crm"
 64)
 65
 66print(presence)
 67```
 68"""
 69from datetime import datetime, timedelta
 70from typing import Literal, Union
 71from dateutil.relativedelta import relativedelta
 72
 73CalendarUnit = Literal["seconds", "minutes", "hours", "days", "months", "quarters", "years"]
 74
 75class TimeModel:
 76
 77
 78    def __init__(self, origin: datetime, unit: CalendarUnit = "seconds"):
 79        self.origin = origin
 80        self.unit = unit
 81
 82    def to_float(self, t: datetime) -> float:
 83        delta = t - self.origin
 84        seconds = delta.total_seconds()
 85
 86        if self.unit == "seconds":
 87            return seconds
 88        if self.unit == "minutes":
 89            return seconds / 60.0
 90        if self.unit == "hours":
 91            return seconds / 3600.0
 92        if self.unit == "days":
 93            return delta.days + (delta.seconds / 86400.0)
 94        if self.unit == "months":
 95            return self._months_between(self.origin, t)
 96        if self.unit == "quarters":
 97            return self._months_between(self.origin, t) / 3.0
 98        if self.unit == "years":
 99            return self._months_between(self.origin, t) / 12.0
100
101        raise ValueError(f"Unsupported time unit: {self.unit}")
102
103    def from_float(self, t: float) -> datetime:
104        if self.unit == "seconds":
105            return self.origin + timedelta(seconds=t)
106        if self.unit == "minutes":
107            return self.origin + timedelta(minutes=t)
108        if self.unit == "hours":
109            return self.origin + timedelta(hours=t)
110        if self.unit == "days":
111            return self.origin + timedelta(days=t)
112        if self.unit in {"months", "quarters", "years"}:
113            months = {
114                "months": int(round(t)),
115                "quarters": int(round(t * 3)),
116                "years": int(round(t * 12)),
117            }[self.unit]
118            return self.origin + relativedelta(months=+months)
119
120        raise ValueError(f"Unsupported time unit: {self.unit}")
121
122    @staticmethod
123    def _months_between(d1: datetime, d2: datetime) -> float:
124        """Return the number of calendar months between two dates, fractional."""
125        rd = relativedelta(d2, d1)
126        return rd.years * 12 + rd.months + (rd.days / 30.0)
CalendarUnit = typing.Literal['seconds', 'minutes', 'hours', 'days', 'months', 'quarters', 'years']
class TimeModel:
 76class TimeModel:
 77
 78
 79    def __init__(self, origin: datetime, unit: CalendarUnit = "seconds"):
 80        self.origin = origin
 81        self.unit = unit
 82
 83    def to_float(self, t: datetime) -> float:
 84        delta = t - self.origin
 85        seconds = delta.total_seconds()
 86
 87        if self.unit == "seconds":
 88            return seconds
 89        if self.unit == "minutes":
 90            return seconds / 60.0
 91        if self.unit == "hours":
 92            return seconds / 3600.0
 93        if self.unit == "days":
 94            return delta.days + (delta.seconds / 86400.0)
 95        if self.unit == "months":
 96            return self._months_between(self.origin, t)
 97        if self.unit == "quarters":
 98            return self._months_between(self.origin, t) / 3.0
 99        if self.unit == "years":
100            return self._months_between(self.origin, t) / 12.0
101
102        raise ValueError(f"Unsupported time unit: {self.unit}")
103
104    def from_float(self, t: float) -> datetime:
105        if self.unit == "seconds":
106            return self.origin + timedelta(seconds=t)
107        if self.unit == "minutes":
108            return self.origin + timedelta(minutes=t)
109        if self.unit == "hours":
110            return self.origin + timedelta(hours=t)
111        if self.unit == "days":
112            return self.origin + timedelta(days=t)
113        if self.unit in {"months", "quarters", "years"}:
114            months = {
115                "months": int(round(t)),
116                "quarters": int(round(t * 3)),
117                "years": int(round(t * 12)),
118            }[self.unit]
119            return self.origin + relativedelta(months=+months)
120
121        raise ValueError(f"Unsupported time unit: {self.unit}")
122
123    @staticmethod
124    def _months_between(d1: datetime, d2: datetime) -> float:
125        """Return the number of calendar months between two dates, fractional."""
126        rd = relativedelta(d2, d1)
127        return rd.years * 12 + rd.months + (rd.days / 30.0)
TimeModel( origin: datetime.datetime, unit: Literal['seconds', 'minutes', 'hours', 'days', 'months', 'quarters', 'years'] = 'seconds')
79    def __init__(self, origin: datetime, unit: CalendarUnit = "seconds"):
80        self.origin = origin
81        self.unit = unit
origin
unit
def to_float(self, t: datetime.datetime) -> float:
 83    def to_float(self, t: datetime) -> float:
 84        delta = t - self.origin
 85        seconds = delta.total_seconds()
 86
 87        if self.unit == "seconds":
 88            return seconds
 89        if self.unit == "minutes":
 90            return seconds / 60.0
 91        if self.unit == "hours":
 92            return seconds / 3600.0
 93        if self.unit == "days":
 94            return delta.days + (delta.seconds / 86400.0)
 95        if self.unit == "months":
 96            return self._months_between(self.origin, t)
 97        if self.unit == "quarters":
 98            return self._months_between(self.origin, t) / 3.0
 99        if self.unit == "years":
100            return self._months_between(self.origin, t) / 12.0
101
102        raise ValueError(f"Unsupported time unit: {self.unit}")
def from_float(self, t: float) -> datetime.datetime:
104    def from_float(self, t: float) -> datetime:
105        if self.unit == "seconds":
106            return self.origin + timedelta(seconds=t)
107        if self.unit == "minutes":
108            return self.origin + timedelta(minutes=t)
109        if self.unit == "hours":
110            return self.origin + timedelta(hours=t)
111        if self.unit == "days":
112            return self.origin + timedelta(days=t)
113        if self.unit in {"months", "quarters", "years"}:
114            months = {
115                "months": int(round(t)),
116                "quarters": int(round(t * 3)),
117                "years": int(round(t * 12)),
118            }[self.unit]
119            return self.origin + relativedelta(months=+months)
120
121        raise ValueError(f"Unsupported time unit: {self.unit}")