forecose 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ dist/
2
+ pydexcom.egg-info/
3
+ __pycache__/
4
+ .venv/
5
+ build/
6
+ .mypy_cache/
7
+ .pytest_cache/
8
+ .DS_Store
9
+ .ruff_cache
10
+ uv.lock
11
+ docs/
@@ -0,0 +1,3 @@
1
+ {
2
+ "python-envs.defaultEnvManager": "ms-python.python:system"
3
+ }
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: forecose
3
+ Version: 0.1.0
4
+ Summary: A time-series forecasting extension for pydexcom using Google's TimesFM
5
+ Author: Alexander Sadler
6
+ Requires-Python: >=3.9
7
+ Requires-Dist: numpy
8
+ Requires-Dist: pandas
9
+ Requires-Dist: pydexcom
10
+ Requires-Dist: timesfm[torch]>=2.0.0
11
+ Description-Content-Type: text/markdown
12
+
13
+ A time-series forecasting extension for [pydexcom](https://github.com/gagebenne/pydexcom) using Google's [TimesFM](https://github.com/google-research/timesfm). Used to predict immediate, short term blood glucose readings.
14
+
15
+ > All modelling and forecasting is performed locally on your device. The only external connections made are with:
16
+ > - Dexcom Share API: fetching CGM readings following the `pydexcom` approach.
17
+ > - HuggingFace: one-time download of the forecasting model weights on the first run.
18
+
19
+ ## Quick Start
20
+ 1. Ensure that you have installed the pydexcom package and [enabled the Share service](https://provider.dexcom.com/education-research/cgm-education-use/videos/setting-dexcom-share-and-follow) within your [Dexcom G7 / G6 / G5 / G4](https://www.dexcom.com/apps).
21
+
22
+ `pip install pydexcom`
23
+
24
+ 2. Initialise `pydexcom` with your Dexcom credentials (below shows the simplist route, refere to pydexcom for further instruction).
25
+
26
+ ```python
27
+ >>> from pydexcom import Dexcom
28
+ >>> dexcom = Dexcom(username="username", password="password")
29
+ ```
30
+
31
+ 3. Generate a prediction.
32
+
33
+ ```python
34
+ >>> from forecose import DexcomForecast
35
+ >>> forecaster = DexcomForecast.from_dexcom(
36
+ dexcom=dexcom, # pull recent readings from your active 'Dexcom' session
37
+ context_len=288, # uses prior day's readings as context
38
+ horizon=12 # predicts the next hour
39
+ )
40
+ >>> predictions = forecaster.forecast()
41
+ >>> print(predictions.head())
42
+ timestamp predicted_glucose q10 q25 q50 q75 q90
43
+ 0 2026-06-25 11:01:19.199000+01:00 8.243370 8.244899 8.125346 8.214749 8.266071 8.309934
44
+ 1 2026-06-25 11:06:19.199000+01:00 8.050682 8.073329 7.738788 8.018662 8.208736 8.303193
45
+ 2 2026-06-25 11:11:19.199000+01:00 7.897943 7.879324 7.332723 7.783697 8.082028 8.256586
46
+ 3 2026-06-25 11:16:19.199000+01:00 7.767045 7.738261 6.965607 7.621467 8.026337 8.236394
47
+ 4 2026-06-25 11:21:19.199000+01:00 7.615633 7.667328 6.668064 7.442780 7.972524 8.216294
48
+ ```
@@ -0,0 +1,36 @@
1
+ A time-series forecasting extension for [pydexcom](https://github.com/gagebenne/pydexcom) using Google's [TimesFM](https://github.com/google-research/timesfm). Used to predict immediate, short term blood glucose readings.
2
+
3
+ > All modelling and forecasting is performed locally on your device. The only external connections made are with:
4
+ > - Dexcom Share API: fetching CGM readings following the `pydexcom` approach.
5
+ > - HuggingFace: one-time download of the forecasting model weights on the first run.
6
+
7
+ ## Quick Start
8
+ 1. Ensure that you have installed the pydexcom package and [enabled the Share service](https://provider.dexcom.com/education-research/cgm-education-use/videos/setting-dexcom-share-and-follow) within your [Dexcom G7 / G6 / G5 / G4](https://www.dexcom.com/apps).
9
+
10
+ `pip install pydexcom`
11
+
12
+ 2. Initialise `pydexcom` with your Dexcom credentials (below shows the simplist route, refere to pydexcom for further instruction).
13
+
14
+ ```python
15
+ >>> from pydexcom import Dexcom
16
+ >>> dexcom = Dexcom(username="username", password="password")
17
+ ```
18
+
19
+ 3. Generate a prediction.
20
+
21
+ ```python
22
+ >>> from forecose import DexcomForecast
23
+ >>> forecaster = DexcomForecast.from_dexcom(
24
+ dexcom=dexcom, # pull recent readings from your active 'Dexcom' session
25
+ context_len=288, # uses prior day's readings as context
26
+ horizon=12 # predicts the next hour
27
+ )
28
+ >>> predictions = forecaster.forecast()
29
+ >>> print(predictions.head())
30
+ timestamp predicted_glucose q10 q25 q50 q75 q90
31
+ 0 2026-06-25 11:01:19.199000+01:00 8.243370 8.244899 8.125346 8.214749 8.266071 8.309934
32
+ 1 2026-06-25 11:06:19.199000+01:00 8.050682 8.073329 7.738788 8.018662 8.208736 8.303193
33
+ 2 2026-06-25 11:11:19.199000+01:00 7.897943 7.879324 7.332723 7.783697 8.082028 8.256586
34
+ 3 2026-06-25 11:16:19.199000+01:00 7.767045 7.738261 6.965607 7.621467 8.026337 8.236394
35
+ 4 2026-06-25 11:21:19.199000+01:00 7.615633 7.667328 6.668064 7.442780 7.972524 8.216294
36
+ ```
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["hatchling", "hatch-vcs"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "forecose"
7
+ version = "0.1.0"
8
+ authors = [
9
+ {name = "Alexander Sadler"}
10
+ ]
11
+ description = "A time-series forecasting extension for pydexcom using Google's TimesFM"
12
+ readme = "README.md"
13
+ requires-python = ">=3.9"
14
+ dependencies = [
15
+ "numpy",
16
+ "pandas",
17
+ "pydexcom",
18
+ "timesfm[torch]>=2.0.0"
19
+ ]
@@ -0,0 +1,5 @@
1
+ from .forecose import DexcomForecast
2
+
3
+ __all__ = [
4
+ "DexcomForecast"
5
+ ]
@@ -0,0 +1,116 @@
1
+ """Dexcom prediction class."""
2
+
3
+ import numpy as np
4
+
5
+ import pandas as pd
6
+
7
+ import timesfm
8
+
9
+ from pydexcom import Dexcom
10
+
11
+ class DexcomForecast:
12
+ """Class for a time-series forecasting model construction of CGM data."""
13
+
14
+ def __init__(
15
+ self,
16
+ cgm_history: np.ndarray,
17
+ last_timestamp: pd.Timestamp,
18
+ sampling_interval: pd.Timedelta,
19
+ context_len: int = 288,
20
+ horizon: int = 12
21
+ ) -> None:
22
+ """
23
+ Compiles the TimesFM model.
24
+ """
25
+ self.context_len = context_len
26
+ self.horizon = horizon
27
+
28
+ self.cgm_history = cgm_history[-self.context_len:]
29
+ self.last_timestamp = last_timestamp
30
+ self.sampling_interval = sampling_interval
31
+
32
+ # initalise and compile the TimesFM model
33
+ self.model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
34
+ "google/timesfm-2.5-200m-pytorch"
35
+ )
36
+ self.model.compile(
37
+ timesfm.ForecastConfig(
38
+ max_context=self.context_len,
39
+ max_horizon=self.horizon,
40
+ normalize_inputs=True,
41
+ use_continuous_quantile_head=True,
42
+ infer_is_positive=True,
43
+ fix_quantile_crossing=True,
44
+ )
45
+ )
46
+
47
+ @classmethod
48
+ def from_dexcom(
49
+ cls,
50
+ dexcom: Dexcom,
51
+ context_len: int = 288,
52
+ horizon: int = 12
53
+ ):
54
+ """Pull data from an active pydexcom session."""
55
+ if not isinstance(dexcom, Dexcom):
56
+ raise TypeError("Expected an object of type pydexcom.Dexcom.")
57
+
58
+ # extract data
59
+ readings = dexcom.get_glucose_readings(
60
+ minutes=1440, max_count=context_len
61
+ )
62
+ if not readings:
63
+ raise RuntimeError("No readings returned from Dexcom Share API.")
64
+
65
+ # process data structures
66
+ df = pd.DataFrame(
67
+ [{"Time": r.datetime, "Glucose": r.mmol_l} for r in reversed(readings)]
68
+ )
69
+ df["Time"] = pd.to_datetime(df["Time"], utc=True).dt.tz_convert("Europe/London")
70
+ df = df.sort_values("Time").reset_index(drop=True)
71
+
72
+ cgm_history = df["Glucose"].to_numpy(dtype=float)
73
+ last_timestamp = df["Time"].iloc[-1]
74
+ sampling_interval = pd.Timedelta(df["Time"].diff().median())
75
+
76
+ # return instance
77
+ return cls(
78
+ cgm_history=cgm_history,
79
+ last_timestamp=last_timestamp,
80
+ sampling_interval=sampling_interval,
81
+ context_len=context_len,
82
+ horizon=horizon
83
+ )
84
+
85
+ def forecast(self) -> pd.DataFrame:
86
+ """Returns point and quantile forecasts of time series data."""
87
+ if len(self.cgm_history) == 0:
88
+ raise ValueError("No historical data provided for forecasting.")
89
+
90
+ # run inference
91
+ point_forecast, quantile_forecast = self.model.forecast(
92
+ horizon=self.horizon,
93
+ inputs=[self.cgm_history],
94
+ )
95
+
96
+ point_vals = point_forecast[0]
97
+ quant_vals = quantile_forecast[0]
98
+
99
+ future_timestamps = pd.date_range(
100
+ start=self.last_timestamp + self.sampling_interval,
101
+ periods=self.horizon,
102
+ freq=self.sampling_interval,
103
+ )
104
+
105
+ # compile
106
+ forecast_df = pd.DataFrame({
107
+ "timestamp": future_timestamps,
108
+ "predicted_glucose": np.clip(point_vals, *(2.2, 22.2)),
109
+ "q10": np.clip(quant_vals[:, 0], *(2.2, 22.2)),
110
+ "q25": np.clip(quant_vals[:, 1], *(2.2, 22.2)),
111
+ "q50": np.clip(quant_vals[:, 4], *(2.2, 22.2)),
112
+ "q75": np.clip(quant_vals[:, 7], *(2.2, 22.2)),
113
+ "q90": np.clip(quant_vals[:, 8], *(2.2, 22.2)),
114
+ })
115
+
116
+ return forecast_df
@@ -0,0 +1,64 @@
1
+ import pytest
2
+ import numpy as np
3
+ import pandas as pd
4
+ from unittest.mock import patch, MagicMock
5
+ from forecose import DexcomForecast
6
+
7
+ @pytest.fixture
8
+ def sample_cgm_data():
9
+ """Sample CGM data for prior 24 hours."""
10
+ return np.random.uniform(4.0, 10.0, size=288)
11
+
12
+ @pytest.fixture
13
+ def mock_timesfm_model():
14
+ """
15
+ Mocks the TimesFM model to return dummy arrays.
16
+ """
17
+ with patch("forecose.forecose.timesfm.TimesFM_2p5_200M_torch.from_pretrained") as mock_model:
18
+ instance = mock_model.return_value
19
+ mock_point = np.array([[5.5] * 12])
20
+ mock_quantiles = np.zeros((1, 12, 10))
21
+
22
+ # quantiles
23
+ mock_quantiles[0, :, 0] = 4.0
24
+ mock_quantiles[0, :, 1] = 4.5
25
+ mock_quantiles[0, :, 4] = 5.0
26
+ mock_quantiles[0, :, 7] = 6.5
27
+ mock_quantiles[0, :, 8] = 7.0
28
+
29
+ instance.forecast.return_value = (mock_point, mock_quantiles)
30
+ yield instance
31
+
32
+ def test_forecaster_structure(sample_cgm_data, mock_timesfm_model):
33
+ """Verifies the forecast method returns the correct pd.DataFrame structure."""
34
+
35
+ last_time = pd.Timestamp.now(tz="UTC")
36
+ interval = pd.Timedelta(minutes=5)
37
+
38
+ forecaster = DexcomForecast(
39
+ cgm_history=sample_cgm_data,
40
+ last_timestamp=last_time,
41
+ sampling_interval=interval,
42
+ context_len=288,
43
+ horizon=12
44
+ )
45
+
46
+ df = forecaster.forecast()
47
+
48
+ assert len(df) == 12
49
+
50
+ assert list(df.columns) == ["timestamp", "predicted_glucose", "q10", "q25", "q50", "q75", "q90"]
51
+
52
+ assert df["timestamp"].iloc[0] == last_time + interval
53
+ assert df["timestamp"].iloc[1] == last_time + (2 * interval)
54
+
55
+ def test_forecaster_empty_history():
56
+ """Verifies the model blocks execution if no data is passed from the Dexcom Share API."""
57
+ forecaster = DexcomForecast(
58
+ cgm_history=np.array([]),
59
+ last_timestamp=pd.Timestamp.now(tz="UTC"),
60
+ sampling_interval=pd.Timedelta(minutes=5)
61
+ )
62
+
63
+ with pytest.raises(ValueError, match="No historical data provided"):
64
+ forecaster.forecast()