bioweather / app.py
emp-admin's picture
Upload 9 files
5f98f88 verified
"""
═══════════════════════════════════════════════════════════════════════
Phoebe Bioweather API v2.0
EmpedocLabs Β© 2025
Weather-driven headache risk scoring + actionable clinical advice.
Designed for the Phoebe iOS app.
GET / β†’ Status
GET /health β†’ Model status
POST /predict β†’ Risk score + condition + personalized actions
═══════════════════════════════════════════════════════════════════════
"""
import logging
import os
import pickle
import numpy as np
import pandas as pd
from typing import List
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
# ── Logging ──────────────────────────────────────────────────────────
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
logger = logging.getLogger("bioweather")
# ── App ──────────────────────────────────────────────────────────────
app = FastAPI(
title="Phoebe Bioweather API",
version="2.0.0",
description="Weather-driven headache risk scoring for the Phoebe iOS app by EmpedocLabs.",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ── Models ───────────────────────────────────────────────────────────
risk_model = None
advice_model = None
FEATURE_COLS = ["temp_c", "pressure_hpa", "humidity", "wind_kph",
"uv_index", "pressure_drop", "temp_change"]
@app.on_event("startup")
async def load_models():
global risk_model, advice_model
for name, filename in [("risk", "risk_model.pkl"), ("advice", "advice_model.pkl")]:
path = filename
if not os.path.exists(path):
path = os.path.join("model", filename)
if not os.path.exists(path):
path = os.path.join(os.path.dirname(__file__), filename)
try:
with open(path, "rb") as f:
if name == "risk":
risk_model = pickle.load(f)
else:
advice_model = pickle.load(f)
logger.info(f"βœ… {name}_model loaded from {path}")
except Exception as e:
logger.error(f"❌ Failed to load {name}_model: {e}")
if risk_model and advice_model:
logger.info("βœ… Bioweather v2.0 ready")
# ═══════════════════════════════════════════════════════════════════════
# ADVICE LIBRARY β€” 15 biometeo conditions with 3 severity tiers each
# ═══════════════════════════════════════════════════════════════════════
ADVICE_LIBRARY = {
0: {
"title": "Clear Skies, Clear Head",
"emoji": "🌿",
"texts": {
"Low": "Weather-driven headache pressure is low today. If symptoms show up, hydration, sleep debt, skipped meals, stress, or screen strain are more likely drivers than the atmosphere.",
"Moderate": "The weather pattern is mostly calm, but your combined inputs still show some vulnerability. Keep meals regular, hydrate early, and avoid stacking other triggers on top.",
"High": "The atmosphere itself looks relatively stable, but your overall risk is still elevated. Treat this like a threshold-protection day: reduce glare, hydrate aggressively, and keep your routine steady."
},
"actions": [
"Keep hydration and meals consistent.",
"Use this lower-weather-stress window for demanding work, but do not overdo screens.",
"If symptoms appear, check non-weather triggers first: sleep, caffeine timing, stress, or dehydration."
]
},
1: {
"title": "Rapid Pressure Drop",
"emoji": "πŸ“‰",
"texts": {
"Low": "Pressure is dipping, but the signal is still mild. Very sensitive users may notice slight heaviness behind the eyes or a drop in energy.",
"Moderate": "Pressure is falling fast enough to lower your migraine threshold. This is a day to reduce other triggers and keep hydration steady.",
"High": "A sharp pressure drop is one of today's main headache drivers. Lower sensory load, keep rescue medication accessible if prescribed, and avoid overexertion."
},
"actions": [
"Lower sensory load: dim lights, shorter screen blocks, less noise.",
"Keep hydration steady and avoid skipped meals.",
"If you use rescue medication under clinician guidance, keep it nearby."
]
},
2: {
"title": "The Pressure Squeeze",
"emoji": "πŸ“ˆ",
"texts": {
"Low": "Pressure is rising gradually. You may feel mild facial tightness or subtle ear pressure, but the trigger strength is still limited.",
"Moderate": "A meaningful pressure rise can create sinus and jaw tension. Support your body with hydration, posture awareness, and short breaks from screens.",
"High": "A strong pressure rise is likely compressing sinus and facial tissues enough to trigger discomfort. Reduce jaw clenching, use warmth, and protect the rest of the day from extra triggers."
},
"actions": [
"Relax your jaw and avoid clenching.",
"Try a warm compress over the face or around the eyes.",
"Take short posture breaks to reduce neck and forehead tension."
]
},
3: {
"title": "The 'Sauna' Effect",
"emoji": "πŸ₯΅",
"texts": {
"Low": "Heat and humidity are present, but the current trigger load is still manageable. Hydration matters more than usual today.",
"Moderate": "Warm, humid air is increasing dehydration and vascular stress. Pre-hydrate and spend more time in cooler, shaded spaces.",
"High": "Hot, heavy air is a major driver today. Your best move is to protect hydration, electrolytes, and body temperature before symptoms ramp up."
},
"actions": [
"Drink water early and add electrolytes if you are sweating.",
"Prefer cool, shaded, or air-conditioned spaces.",
"Avoid hard outdoor exercise during the hottest part of the day."
]
},
4: {
"title": "High Wind Warning",
"emoji": "🌬️",
"texts": {
"Low": "Wind is noticeable and may create mild sensory irritation, especially around the ears and neck.",
"Moderate": "Strong wind can raise headache risk through tension, sensory overload, and rapid local pressure changes around the ears.",
"High": "Wind is one of the main drivers today. Protect your ears and neck, limit exposure, and actively reduce shoulder and jaw tension."
},
"actions": [
"Protect ears and neck outdoors.",
"Watch for shoulder elevation and jaw clenching.",
"Reduce time in open, exposed outdoor spaces."
]
},
5: {
"title": "High UV Glare",
"emoji": "😎",
"texts": {
"Low": "Brightness is elevated, so light-sensitive users may still want a bit more visual protection than usual.",
"Moderate": "High UV and glare can push visual strain, photophobia, and screen fatigue. Protect your eyes early rather than after symptoms start.",
"High": "Strong glare is a major trigger today. Use sunglasses outdoors, cut visual overload indoors, and avoid long unbroken screen sessions."
},
"actions": [
"Wear polarized sunglasses outdoors.",
"Lower screen brightness and use shorter screen intervals.",
"Avoid bright reflections from glass, water, or white surfaces."
]
},
6: {
"title": "Bitter Cold Tension",
"emoji": "❄️",
"texts": {
"Low": "Cold air may cause mild tightening in the scalp, neck, or face, especially if you are already tense.",
"Moderate": "Cold exposure is likely increasing muscle guarding and vascular constriction. Keep your neck and face warm and avoid abrupt exposure.",
"High": "Bitter cold is a strong trigger today. Warm the air you breathe, protect your neck, and treat upper-body tension before it turns into a full headache."
},
"actions": [
"Cover your neck and, if needed, nose and mouth outdoors.",
"Use warm drinks or warmth after exposure.",
"Massage jaw, temples, and upper neck after coming inside."
]
},
7: {
"title": "Drastic Temp Drop",
"emoji": "πŸ₯Ά",
"texts": {
"Low": "A cooler shift is underway, but the risk signal is still limited. Sensitive users may feel mild stiffness or heaviness.",
"Moderate": "A real temperature drop is stressing your system and can tighten muscles and narrow blood vessels. Keep your environment steady and your meals regular.",
"High": "A sharp temperature drop is a key risk factor today. Layer up, avoid abrupt exposure, and reduce extra stressors that compound the vascular shift."
},
"actions": [
"Dress in layers and avoid sudden cold exposure.",
"Do not skip meals while your body is adapting.",
"Use warmth on shoulders or neck if tension starts building."
]
},
8: {
"title": "Heat Shock",
"emoji": "🌑️",
"texts": {
"Low": "A warmer shift is starting, and sensitive users may notice slight fatigue or early throbbing if hydration slips.",
"Moderate": "A meaningful heat jump is pushing vasodilation and dehydration risk. Back off intense activity and replace fluids earlier than usual.",
"High": "A sharp temperature spike is a major trigger today. Stay cool, protect hydration, and slow your pace before the throbbing escalates."
},
"actions": [
"Stay in cooler environments when possible.",
"Hydrate early and consider electrolytes.",
"Avoid intense workouts or long periods in direct heat."
]
},
9: {
"title": "Heavy Dampness",
"emoji": "🌫️",
"texts": {
"Low": "Moist, stagnant air may cause mild heaviness or slight brain fog in sensitive users.",
"Moderate": "Heavy dampness can worsen sinus pressure, lethargy, and that classic 'heavy head' feeling. Keep air moving and your posture clean.",
"High": "Thick, wet air is likely contributing to congestion, fatigue, and dull head pressure today. Improve airflow indoors and limit slumped posture."
},
"actions": [
"Use ventilation or a dehumidifier if available.",
"Take movement breaks to avoid slouching.",
"Try steam, saline, or light breathwork if sinus pressure builds."
]
},
10: {
"title": "Mild Barometric Dip",
"emoji": "πŸ“‰",
"texts": {
"Low": "A small pressure dip is present. Many people will feel nothing, but highly sensitive users may notice subtle dullness or eye pressure.",
"Moderate": "A mild barometric drop is enough to lower your threshold a bit. Stay ahead on hydration and avoid combining it with skipped meals or long screen blocks.",
"High": "Even though the pressure drop is not extreme, your combined risk is elevated. Protect the basics today and avoid stacking preventable triggers."
},
"actions": [
"Hydrate steadily across the day.",
"Avoid long, uninterrupted screen sessions.",
"Keep meals and caffeine timing consistent."
]
},
11: {
"title": "Mild Pressure Squeeze",
"emoji": "🌀️",
"texts": {
"Low": "Pressure is creeping upward, which may create only mild tightness or ear fullness for sensitive users.",
"Moderate": "A gentle pressure rise can still increase jaw, face, or sinus tension. Stay aware of clenching and support your posture.",
"High": "The pressure rise is modest, but your overall risk is not. Prevent tension build-up early with warmth, breaks, and active jaw relaxation."
},
"actions": [
"Relax your jaw and separate your teeth.",
"Use warmth if facial tightness starts.",
"Take short neck and shoulder mobility breaks."
]
},
12: {
"title": "Breezy Pollen Risk",
"emoji": "🌾",
"texts": {
"Low": "Air movement may be stirring light environmental irritation, especially if you already have mild allergy sensitivity.",
"Moderate": "Breezy conditions can carry pollen and dust that push sinus and histamine-related headaches. Keep indoor air cleaner and limit exposure if needed.",
"High": "Wind-driven allergen exposure is likely one of today's main triggers. Protect your airways, keep windows controlled, and manage the histamine load early."
},
"actions": [
"Keep windows closed if pollen is a usual issue.",
"Wash face and rinse nasal passages after time outside.",
"Use an air purifier or cleaner indoor airflow when possible."
]
},
13: {
"title": "Dry Air Warning",
"emoji": "🏜️",
"texts": {
"Low": "Dry air is present and may quietly increase thirst, eye strain, or sinus irritation without feeling dramatic at first.",
"Moderate": "Dry air is now meaningful enough to support dehydration and sinus discomfort. Sip fluids consistently and do not wait for thirst.",
"High": "Very dry air is likely pushing dehydration and airway irritation today. Rehydrate early, reduce diuretics, and support your sinuses before pain builds."
},
"actions": [
"Drink water consistently, not in one big catch-up.",
"Use a humidifier or saline spray if your nose feels dry.",
"Go easy on excess caffeine or alcohol."
]
},
14: {
"title": "Stagnant & Gloomy",
"emoji": "☁️",
"texts": {
"Low": "The weather is flat and dull, which may lead to low energy more than direct headache pressure.",
"Moderate": "Still, gloomy weather can reduce movement, worsen posture, and increase brain fog. A little structured movement will help more than you think.",
"High": "The atmosphere is not violent, but the stagnation is clearly affecting your threshold. Fight lethargy on purpose: move, hydrate, improve lighting, and protect posture."
},
"actions": [
"Get light movement every 45–60 minutes.",
"Improve indoor lighting if screens feel heavy on the eyes.",
"Watch posture and avoid collapsing into the desk."
]
},
}
# ═══════════════════════════════════════════════════════════════════════
# REQUEST / RESPONSE
# ═══════════════════════════════════════════════════════════════════════
class WeatherInput(BaseModel):
temp_c: float = Field(..., description="Temperature in Celsius")
pressure_hpa: float = Field(..., description="Barometric pressure in hPa/mbar")
humidity: float = Field(..., description="Relative humidity %")
wind_kph: float = Field(..., description="Wind speed km/h")
uv_index: int = Field(..., ge=0, le=11, description="UV index 0-11")
pressure_drop: float = Field(..., description="24h pressure change in hPa (negative = drop)")
temp_change: float = Field(..., description="24h temperature change in Β°C")
class ConditionResponse(BaseModel):
id: int
title: str
emoji: str
text: str
actions: List[str]
class PredictResponse(BaseModel):
risk_score: int
risk_level: str
condition: ConditionResponse
# ═══════════════════════════════════════════════════════════════════════
# LOGIC
# ═══════════════════════════════════════════════════════════════════════
def clamp_risk(value) -> int:
try:
return int(max(0, min(100, round(float(value)))))
except Exception:
return 0
def get_risk_level(score: int) -> str:
if score > 55: return "High"
if score > 30: return "Moderate"
return "Low"
def infer_rule_condition(row: dict) -> tuple:
"""Rule-based coherence β€” corrects ML when physics is obvious."""
temp = float(row["temp_c"])
hum = float(row["humidity"])
wind = float(row["wind_kph"])
uv = int(row["uv_index"])
pd_ = float(row["pressure_drop"])
tc = float(row["temp_change"])
cands = []
if pd_ <= -8: cands.append((1, 95))
elif pd_ <= -4: cands.append((10, 72))
if pd_ >= 8: cands.append((2, 95))
elif pd_ >= 4: cands.append((11, 72))
if temp >= 29 and hum >= 70: cands.append((3, 92))
if wind >= 40: cands.append((4, 90))
elif wind >= 20: cands.append((12, 66))
if uv >= 8: cands.append((5, 88))
if temp <= 0: cands.append((6, 84))
if tc <= -7: cands.append((7, 89))
elif tc >= 7: cands.append((8, 89))
if hum >= 92 and wind <= 12: cands.append((9, 76))
if hum <= 30: cands.append((13, 78))
if uv <= 2 and hum >= 75 and wind <= 10: cands.append((14, 64))
if not cands:
return 0, 0
return max(cands, key=lambda x: x[1])
def select_condition(ml_id: int, row: dict, risk: int) -> int:
"""ML first, rules correct obvious mismatches."""
rule_id, strength = infer_rule_condition(row)
if ml_id not in ADVICE_LIBRARY:
return rule_id if strength > 0 else 0
if strength >= 90 and ml_id != rule_id:
return rule_id
if ml_id == 0 and risk >= 45 and strength >= 65:
return rule_id
return ml_id
def build_actions(cond_id: int, risk: int, row: dict) -> List[str]:
level = get_risk_level(risk)
acts = []
if level == "High":
acts.extend([
"Reduce stimulation for the next few hours: dim lights, lower audio, and shorten screen sessions.",
"Keep hydration, food intake, and routine stable today.",
"If you have a clinician-approved rescue plan, keep it accessible.",
])
elif level == "Moderate":
acts.extend([
"Protect the basics early: hydration, meals, and shorter screen blocks.",
"Avoid stacking other triggers like dehydration, long fasting, or poor posture.",
])
else:
acts.append("No need to overreact, but stay consistent with hydration and meals.")
acts.extend(ADVICE_LIBRARY[cond_id]["actions"])
if row["uv_index"] >= 7:
acts.append("Use sunglasses outdoors and reduce glare indoors.")
if row["humidity"] >= 70 and row["temp_c"] >= 27:
acts.append("Prioritize electrolytes and cooler environments.")
if row["humidity"] <= 30:
acts.append("Support dry sinuses with humidity or saline if needed.")
if row["wind_kph"] >= 25:
acts.append("Protect your ears and neck when outside.")
if abs(row["temp_change"]) >= 7:
acts.append("Avoid abrupt indoor/outdoor temperature swings; transition gradually.")
if abs(row["pressure_drop"]) >= 4:
acts.append("Keep the rest of the day trigger-light: no skipped meals, no dehydration, no unnecessary strain.")
# Dedupe keeping order
seen = set()
unique = []
for a in acts:
if a not in seen:
seen.add(a)
unique.append(a)
return unique[:6]
# ═══════════════════════════════════════════════════════════════════════
# ENDPOINTS
# ═══════════════════════════════════════════════════════════════════════
@app.get("/")
def home():
return {
"service": "Phoebe Bioweather API",
"version": "2.0.0",
"by": "EmpedocLabs",
"status": "running" if risk_model and advice_model else "models_not_loaded",
}
@app.get("/health")
def health():
return {
"status": "healthy" if risk_model and advice_model else "degraded",
"risk_model_loaded": risk_model is not None,
"advice_model_loaded": advice_model is not None,
}
@app.post("/predict", response_model=PredictResponse)
def predict(input_data: WeatherInput):
if not risk_model or not advice_model:
raise HTTPException(503, "Models not loaded")
row = input_data.model_dump()
df = pd.DataFrame([row])[FEATURE_COLS]
# 1. Risk score
risk_pred = risk_model.predict(df)[0]
risk_score = clamp_risk(risk_pred)
risk_level = get_risk_level(risk_score)
# 2. Condition from ML
ml_condition = int(advice_model.predict(df)[0])
# 3. Deterministic coherence
condition_id = select_condition(ml_condition, row, risk_score)
content = ADVICE_LIBRARY.get(condition_id, ADVICE_LIBRARY[0])
# 4. Text by risk level
text = content["texts"][risk_level]
# 5. Actions
actions = build_actions(condition_id, risk_score, row)
logger.info(f"Predict: risk={risk_score} ({risk_level}), cond={condition_id} ({content['title']})")
return PredictResponse(
risk_score=risk_score,
risk_level=risk_level,
condition=ConditionResponse(
id=condition_id,
title=content["title"],
emoji=content["emoji"],
text=text,
actions=actions,
),
)