Download backend/core/simulator.py from Shwy1001/BioPatch: direct link, hf CLI and curl.
- Browser
- Download file 4.99 kB
-
https://huggingface.co/spaces/Shwy1001/BioPatch/resolve/main/backend/core/simulator.py
- Command line
-
hf download hf://spaces/Shwy1001/BioPatch/backend/core/simulator.py
-
curl -L -o simulator.py https://huggingface.co/spaces/Shwy1001/BioPatch/resolve/main/backend/core/simulator.py
4.99 kB
| """Dev-only sensor simulator. | |
| Drives the production ingest pipeline (`ingest_and_broadcast`) so that the | |
| dashboard has live data without real IoT hardware. Disabled in production | |
| via the ENABLE_SIMULATOR setting. | |
| The simulator deliberately drifts one patient toward an SSI signature so the | |
| 'critical' UI states are exercisable on first launch. | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import logging | |
| import random | |
| from dataclasses import dataclass | |
| from sqlalchemy import select | |
| from core.database import SessionLocal | |
| from core.realtime import ingest_and_broadcast | |
| from models import Patient, Sensor | |
| logger = logging.getLogger(__name__) | |
| class _Stream: | |
| patient_id: int | |
| sensor_id: int | None | |
| temperature_c: float | |
| ph: float | |
| humidity: float | |
| # Slow drifts that determine the trajectory | |
| temp_drift: float | |
| ph_drift: float | |
| humidity_drift: float | |
| # Battery + signal state | |
| battery_pct: float | |
| signal_dbm: float | |
| def _seed_streams() -> list[_Stream]: | |
| """Build one stream per patient using the seeded sensors. The first | |
| patient is given a clearly deteriorating trajectory; the rest stay near | |
| healing baselines with small noise.""" | |
| streams: list[_Stream] = [] | |
| with SessionLocal() as db: | |
| patients = list(db.scalars(select(Patient).where(Patient.discharged.is_(False))).all()) | |
| for idx, p in enumerate(patients): | |
| sensor = db.scalar(select(Sensor).where(Sensor.patient_id == p.id).limit(1)) | |
| if idx == 0: | |
| # Deteriorating patient — slow drift toward infection signature | |
| streams.append( | |
| _Stream( | |
| patient_id=p.id, | |
| sensor_id=sensor.id if sensor else None, | |
| temperature_c=37.1, | |
| ph=7.05, | |
| humidity=78.0, | |
| temp_drift=+0.012, | |
| ph_drift=+0.010, | |
| humidity_drift=+0.05, | |
| battery_pct=88.0, | |
| signal_dbm=-62.0, | |
| ) | |
| ) | |
| else: | |
| streams.append( | |
| _Stream( | |
| patient_id=p.id, | |
| sensor_id=sensor.id if sensor else None, | |
| temperature_c=random.uniform(33.6, 34.8), | |
| ph=random.uniform(5.8, 6.4), | |
| humidity=random.uniform(70.0, 80.0), | |
| temp_drift=0.0, | |
| ph_drift=0.0, | |
| humidity_drift=0.0, | |
| battery_pct=random.uniform(85.0, 99.0), | |
| signal_dbm=random.uniform(-70.0, -50.0), | |
| ) | |
| ) | |
| return streams | |
| def _step(stream: _Stream) -> None: | |
| stream.temperature_c += stream.temp_drift + random.gauss(0.0, 0.05) | |
| stream.ph += stream.ph_drift + random.gauss(0.0, 0.015) | |
| stream.humidity += stream.humidity_drift + random.gauss(0.0, 0.5) | |
| # Clamp to physically plausible ranges | |
| stream.temperature_c = max(30.0, min(41.0, stream.temperature_c)) | |
| stream.ph = max(4.5, min(9.5, stream.ph)) | |
| stream.humidity = max(35.0, min(99.0, stream.humidity)) | |
| # Slow battery drain + tiny signal jitter | |
| stream.battery_pct = max(5.0, stream.battery_pct - random.uniform(0.0, 0.05)) | |
| stream.signal_dbm = max(-90.0, min(-40.0, stream.signal_dbm + random.gauss(0.0, 1.2))) | |
| async def run_simulator(interval_seconds: float = 6.0) -> None: | |
| streams = _seed_streams() | |
| if not streams: | |
| logger.info("Simulator: no patients to drive, exiting") | |
| return | |
| logger.info("Simulator: driving %d streams every %.1fs", len(streams), interval_seconds) | |
| try: | |
| while True: | |
| for stream in streams: | |
| _step(stream) | |
| with SessionLocal() as db: | |
| sensor = db.get(Sensor, stream.sensor_id) if stream.sensor_id else None | |
| if sensor is not None: | |
| sensor.battery_pct = stream.battery_pct | |
| sensor.signal_dbm = stream.signal_dbm | |
| try: | |
| patient = db.get(Patient, stream.patient_id) | |
| await ingest_and_broadcast( | |
| db, | |
| patient_id=stream.patient_id, | |
| temperature_c=stream.temperature_c, | |
| ph=stream.ph, | |
| humidity=stream.humidity, | |
| sensor=sensor, | |
| patient=patient, | |
| ) | |
| except Exception: | |
| logger.exception("Simulator ingest failed for patient %s", stream.patient_id) | |
| await asyncio.sleep(interval_seconds) | |
| except asyncio.CancelledError: | |
| logger.info("Simulator: cancelled, exiting") | |
| raise | |