Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c6bdd0b
feat(eta): package gtfs_eta inference library with relocatable registry
dotjae Jun 25, 2026
3ccfe14
chore(eta): add placeholder global baseline model for dev/test
dotjae Jun 25, 2026
46f226f
feat(eta): generate stop-time updates from ETA predictions
dotjae Jun 25, 2026
6603390
test(eta): stop-time producer suite
dotjae Jun 25, 2026
064e643
chore(eta): drop fake_stop_times placeholder
dotjae Jun 25, 2026
c5ef42b
chore(tasks): sketch of the HTTP telemetry fetching bridge
fabianabarca Jul 9, 2026
4c55e5b
feat(models): enhance Company and Operator models, add Equipment and …
fabianabarca Jul 20, 2026
5e1c419
feat(operations): migrate Sensor model and drift reconciliation
dotjae Jul 21, 2026
d4dd3f0
feat(operations): register Sensor in the admin site
dotjae Jul 21, 2026
fdaaf15
chore(config): add MQTT_HOST and MQTT_PORT env vars
dotjae Jul 21, 2026
4b13631
feat(realtime): add pluggable HTTP telemetry source adapter package
dotjae Jul 21, 2026
67cbb72
test(realtime): cover HTTP telemetry source adapters
dotjae Jul 21, 2026
117f394
feat(realtime): add fetch_positions task for HTTP telemetry sources
dotjae Jul 21, 2026
c6c5e37
feat(celery): schedule fetch_positions every 10 seconds
dotjae Jul 21, 2026
3301214
chore(schedule_engine): drop obsolete telemetry fetch sketch
dotjae Jul 21, 2026
567ef68
chore(operations): regenerate migrations at container start
dotjae Jul 21, 2026
6936b30
fix(realtime): gate fetch_positions on current_run, not runs:in_progress
dotjae Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .env.dev
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,13 @@ DEBUG=True
DJANGO_SERVE_STATIC=True
LOG_LEVEL=DEBUG
CREATE_SUPERUSER=True
RUN_MIGRATIONS=True
RUN_MIGRATIONS=True

# MQTT Configuration
MQTT_HOST=telemetry-broker
MQTT_PORT=1883

# ETA model registry (dev default; seed once with seed_baseline_model)
MODEL_REGISTRY_DIR=eta_models
ETA_MAX_STOPS=10
ETA_DEFAULT_UNCERTAINTY_S=120
12 changes: 11 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ REDIS_PASSWORD=redispassword
# Google Maps API Key
API_TOKEN=

# MQTT Configuration
MQTT_HOST=telemetry-broker
MQTT_PORT=1883

# HiveMQ Configuration
HIVEMQ_LOG_LEVEL=INFO

Expand Down Expand Up @@ -54,4 +58,10 @@ FLOWER_DOMAIN=tasks.databus.simovilab.com
DOCS_DOMAIN=docs.databus.simovilab.com

# Certificate in production
CERT_RESOLVER=letsencrypt
CERT_RESOLVER=letsencrypt

# ETA model registry (used by gtfs_eta; set to a writable directory)
# Seed the baseline model once: MODEL_REGISTRY_DIR=eta_models ./.venv/bin/python -m gtfs_eta.seed_baseline_model
MODEL_REGISTRY_DIR=eta_models
ETA_MAX_STOPS=3
ETA_DEFAULT_UNCERTAINTY_S=120
4 changes: 4 additions & 0 deletions backend/databus/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@ def debug_task(self):
"task": "realtime_engine.tasks.scan_stale_runs",
"schedule": timedelta(seconds=30),
},
"fetch-positions": {
"task": "realtime_engine.tasks.fetch_positions",
"schedule": timedelta(seconds=10),
},
}
2 changes: 1 addition & 1 deletion backend/docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ wait_for_database() {

run_makemigrations() {
if is_true "${DEBUG:-False}"; then
APPS_TO_MIGRATE=("feed" "schedule_engine" "realtime_engine")
APPS_TO_MIGRATE=("feed" "schedule_engine" "realtime_engine" "operations")
log "Creating migrations for: ${APPS_TO_MIGRATE[*]}"
uv run python manage.py makemigrations "${APPS_TO_MIGRATE[@]}" || warn "No changes detected for migrations"
else
Expand Down
Binary file not shown.
16 changes: 16 additions & 0 deletions backend/eta_models/polyreg_distance_global_baseline_v0_meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"model_type": "polyreg_distance",
"route_id": null,
"route_specific": false,
"degree": 1,
"alpha": 1.0,
"dataset": "synthetic_constant_speed",
"n_samples": 1000,
"metrics": {
"test_mae_seconds": 30.0,
"test_mae_minutes": 0.5
},
"model_key": "polyreg_distance_global_baseline_v0",
"saved_at": "2026-06-25T18:06:35.423137",
"model_path": "polyreg_distance_global_baseline_v0.pkl"
}
10 changes: 10 additions & 0 deletions backend/eta_models/registry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"polyreg_distance_global_baseline_v0": {
"model_path": "polyreg_distance_global_baseline_v0.pkl",
"meta_path": "polyreg_distance_global_baseline_v0_meta.json",
"saved_at": "2026-06-25T18:06:35.423137",
"model_type": "polyreg_distance",
"route_id": null,
"dataset": "synthetic_constant_speed"
}
}
34 changes: 34 additions & 0 deletions backend/gtfs-eta/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# gtfs_eta

Inference-only ETA library: given a vehicle position and the upcoming stops on
its trip, predict arrival times from trained models stored in a model registry.

This is the **consumption half** of the ETA model lifecycle. It is consumed by
databus (`runs/domain/progression/stop_times.py`) to populate the
`run:<id>:stop_time_updates` projection that backs the GTFS-RT trip-updates feed.

## Provenance & intent

- **Vendored, not original.** The canonical source — including model training —
lives in `gtfs-django` (`feature/eta_prediction`). This package is the slimmed
inference half: estimator, feature engineering, and the model registry loader,
with heavy training/serving deps dropped (`xgboost` is an optional extra).
- **Candidate for extraction.** It is namespaced (`gtfs_eta.*`) and databus
depends on it through a single narrow seam (a lazy import in `stop_times.py`
plus the workspace dependency). If a second consumer appears, or it needs an
independent release cadence, it should move to its own package/repo — pulled
the same way `gtfs-io` and `gtfs-django` are — and the move stays mechanical.
Keep the databus → `gtfs_eta` seam narrow to preserve that.

## Model registry

Models are loaded from `MODEL_REGISTRY_DIR` (a `registry.json` index plus per-model
`*.pkl` / `*_meta.json`). Paths are resolved **relative to the registry directory**,
so the registry is relocatable: bind-mount it anywhere, check a placeholder into
version control, or have an external retraining suite write into it.

A deterministic placeholder global baseline can be (re)generated with:

```bash
MODEL_REGISTRY_DIR=eta_models python -m gtfs_eta.seed_baseline_model
```
2 changes: 2 additions & 0 deletions backend/gtfs-eta/gtfs_eta/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""gtfs_eta — namespaced ETA-prediction package for the SIMOVI databus."""
__version__ = "0.1.0"
1 change: 1 addition & 0 deletions backend/gtfs-eta/gtfs_eta/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# gtfs_eta.core
28 changes: 28 additions & 0 deletions backend/gtfs-eta/gtfs_eta/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
Runtime configuration for gtfs_eta.

The registry dir is NOT derived from __file__ — it comes solely from the
MODEL_REGISTRY_DIR environment variable (or the registry's own discovery
logic). This module only exposes the defaults that are safe to use at
import time without side effects.
"""

# Default timezone and region for Costa Rica operations
DEFAULT_TIMEZONE: str = "America/Costa_Rica"
DEFAULT_REGION: str = "CR"

# Weather defaults (used when no live weather feed is available)
DEFAULT_TEMPERATURE_C: float = 25.0
DEFAULT_PRECIPITATION_MM: float = 0.0
DEFAULT_WIND_SPEED_KMH: float | None = None


def get_config() -> dict:
"""Return the active configuration as a plain dict."""
return {
"default_timezone": DEFAULT_TIMEZONE,
"default_region": DEFAULT_REGION,
"default_temperature_c": DEFAULT_TEMPERATURE_C,
"default_precipitation_mm": DEFAULT_PRECIPITATION_MM,
"default_wind_speed_kmh": DEFAULT_WIND_SPEED_KMH,
}
13 changes: 13 additions & 0 deletions backend/gtfs-eta/gtfs_eta/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Custom exceptions for gtfs_eta."""


class GTFSEtaError(Exception):
"""Base error for the gtfs_eta package."""


class ModelNotFoundError(GTFSEtaError):
"""Raised when a requested model is not in the registry."""


class PredictionError(GTFSEtaError):
"""Raised when a model prediction fails."""
15 changes: 15 additions & 0 deletions backend/gtfs-eta/gtfs_eta/core/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Logging helpers for gtfs_eta."""
import logging


def get_logger(name: str, level: str = "INFO") -> logging.Logger:
"""Return a named logger with a consistent format."""
logger = logging.getLogger(name)
if not logger.handlers:
handler = logging.StreamHandler()
handler.setFormatter(
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
)
logger.addHandler(handler)
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
return logger
15 changes: 15 additions & 0 deletions backend/gtfs-eta/gtfs_eta/core/validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Input validation helpers for gtfs_eta."""
from typing import Any


def require_keys(d: dict, keys: list[str], context: str = "") -> None:
"""Raise ValueError if any key is missing from d."""
missing = [k for k in keys if k not in d]
if missing:
raise ValueError(f"Missing required keys {missing} in {context or 'input'}")


def require_positive(value: Any, name: str) -> None:
"""Raise ValueError if value is not a positive number."""
if value is None or float(value) <= 0:
raise ValueError(f"{name} must be a positive number, got {value!r}")
1 change: 1 addition & 0 deletions backend/gtfs-eta/gtfs_eta/eta_service/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# gtfs_eta.eta_service
Loading