Spaces:
Running
Running
| """ | |
| Five-system deterministic scoring engine. | |
| Ported from src/lib/systemScoring.ts and src/lib/stressor-scoring.ts | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from datetime import datetime, timedelta | |
| from typing import Optional | |
| # βββ Types ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| STRESSOR_TYPES = ["alcohol", "sleep", "training", "stress", "ill", "care"] | |
| RECOVERY_SYSTEMS = ["cardiovascular", "brain", "liver", "muscular", "gut"] | |
| SYSTEM_META = { | |
| "cardiovascular": {"label": "Cardiovascular", "icon": "π«", "base_window_hrs": 18}, | |
| "brain": {"label": "Brain / Cognition", "icon": "π§ ", "base_window_hrs": 24}, | |
| "liver": {"label": "Liver", "icon": "π«", "base_window_hrs": 30}, | |
| "muscular": {"label": "Muscular / CNS", "icon": "πͺ", "base_window_hrs": 48}, | |
| "gut": {"label": "Gut", "icon": "π¦ ", "base_window_hrs": 36}, | |
| } | |
| # βββ Stressor definitions βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| STRESSOR_DEFS = { | |
| "alcohol": {"label": "Drank", "icon": "πΊ", "base_points": 32}, | |
| "training": {"label": "Trained", "icon": "πͺ", "base_points": 18}, | |
| "sleep": {"label": "Slept badly", "icon": "π΄", "base_points": 24}, | |
| "stress": {"label": "High stress", "icon": "π€", "base_points": 14}, | |
| "ill": {"label": "Feeling ill", "icon": "π€", "base_points": 35}, | |
| "care": {"label": "Took care of myself", "icon": "β¦", "base_points": -10}, | |
| } | |
| # βββ Modifiers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DRINK_TYPE_MOD = { | |
| "beer": {"liver": 0.8, "brain": 0.4, "gut": 1.3, "cardio": 0.7}, | |
| "red_wine": {"liver": 1.0, "brain": 0.8, "gut": 0.9, "cardio": 0.8}, | |
| "white_wine": {"liver": 1.0, "brain": 0.7, "gut": 0.8, "cardio": 0.7}, | |
| "spirits": {"liver": 1.4, "brain": 1.3, "gut": 1.0, "cardio": 1.1}, | |
| "cocktails": {"liver": 1.3, "brain": 1.4, "gut": 1.2, "cardio": 1.0}, | |
| "champagne": {"liver": 0.9, "brain": 0.6, "gut": 1.0, "cardio": 0.7}, | |
| } | |
| DRINK_COUNT_MOD = {"1-2": 0.5, "3-4": 0.8, "5+": 1.0, "lost_count": 1.2} | |
| TRAINING_CNS = { | |
| "legs": 1.0, | |
| "full_body": 1.0, | |
| "hiit": 0.8, | |
| "cardio": 0.6, | |
| "upper": 0.5, | |
| "mobility": -0.5, | |
| } | |
| TRAINING_CARDIO = { | |
| "hiit": 1.0, | |
| "cardio": 0.9, | |
| "legs": 0.6, | |
| "full_body": 0.7, | |
| "upper": 0.3, | |
| "mobility": -0.3, | |
| } | |
| INTENSITY_MOD = {"easy": 0.4, "hard": 0.85, "destroyed": 1.2} | |
| SLEEP_BRAIN = {"under_4": 1.0, "4-6": 0.75, "6-7": 0.40} | |
| # βββ Science citations ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SCIENCE = { | |
| "liver": { | |
| "fact": "The liver metabolises approximately one standard drink per hour. Processing speed cannot be accelerated by sleep, coffee, or exercise.", | |
| "cite": "Lieber, Physiological Reviews, 1997", | |
| }, | |
| "muscular": { | |
| "fact": "Alcohol consumed within 24 hours of resistance training reduces muscle protein synthesis by up to 37%, even when protein intake is maintained.", | |
| "cite": "Parr et al., PLOS ONE, 2014", | |
| }, | |
| "gut": { | |
| "fact": "A single episode of heavy drinking alters gut microbiome composition within 24 hours, increasing intestinal permeability and systemic inflammation.", | |
| "cite": "Bishehsari et al., Alcohol Research, 2017", | |
| }, | |
| "brain": { | |
| "fact": "Sleep deprivation of even one night impairs prefrontal cortex function equivalently to 0.08% blood alcohol concentration.", | |
| "cite": "Harrison & Horne, Journal of Sleep Research, 2000", | |
| }, | |
| "cardiovascular": { | |
| "fact": "Resting heart rate remains elevated for 12β24 hours after alcohol consumption as the autonomic nervous system works to restore balance.", | |
| "cite": "Spaak et al., Journal of the American College of Cardiology, 2008", | |
| }, | |
| } | |
| # βββ Data classes βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class Stressor: | |
| type: str | |
| alcohol_type: Optional[str] = None | |
| alcohol_count: Optional[str] = None | |
| training_area: Optional[str] = None | |
| training_intensity: Optional[str] = None | |
| sleep_hours: Optional[str] = None | |
| stress_carried: Optional[str] = None | |
| ill_severity: Optional[str] = None | |
| class SystemScore: | |
| system: str | |
| label: str | |
| icon: str | |
| score: int | |
| cleared_at: str | |
| recovery_hrs: float | |
| cause_text: str | |
| action_text: str | |
| science_fact: Optional[str] = None | |
| science_cite: Optional[str] = None | |
| # βββ Live score (quick meter) βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def compute_live_score(stressors: list[Stressor]) -> int: | |
| score = 0 | |
| for s in stressors: | |
| defn = STRESSOR_DEFS.get(s.type) | |
| if not defn: | |
| continue | |
| score += defn["base_points"] | |
| if s.type == "training" and s.training_area == "mobility": | |
| score -= int(defn["base_points"] * 1.5) | |
| if s.type == "training" and s.training_intensity == "destroyed": | |
| score += 8 | |
| if s.type == "alcohol" and s.alcohol_type == "spirits": | |
| score += 6 | |
| if s.type == "alcohol" and s.alcohol_count == "5+": | |
| score += 8 | |
| if s.type == "alcohol" and s.alcohol_count == "lost_count": | |
| score += 12 | |
| return max(0, min(100, score)) | |
| # βββ Five-system scoring ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def compute_system_scores( | |
| stressors: list[Stressor], | |
| now: Optional[datetime] = None, | |
| bed_time: Optional[str] = None, | |
| wake_time: Optional[str] = None, | |
| ) -> list[SystemScore]: | |
| if now is None: | |
| now = datetime.now() | |
| raw = {s: 0.0 for s in RECOVERY_SYSTEMS} | |
| for s in stressors: | |
| if s.type == "alcohol": | |
| drink_mod = DRINK_TYPE_MOD.get(s.alcohol_type or "beer", DRINK_TYPE_MOD["beer"]) | |
| count_mod = DRINK_COUNT_MOD.get(s.alcohol_count or "3-4", 0.8) | |
| base = 30 | |
| raw["liver"] += base * drink_mod["liver"] * count_mod | |
| raw["brain"] += base * drink_mod["brain"] * count_mod | |
| raw["gut"] += base * drink_mod["gut"] * count_mod | |
| raw["cardiovascular"] += base * drink_mod["cardio"] * count_mod * 0.5 | |
| if s.type == "training": | |
| area = s.training_area or "full_body" | |
| intensity = s.training_intensity or "hard" | |
| cns = TRAINING_CNS.get(area, 0.5) * INTENSITY_MOD.get(intensity, 0.85) | |
| cardio = TRAINING_CARDIO.get(area, 0.5) * INTENSITY_MOD.get(intensity, 0.85) | |
| raw["muscular"] += 40 * cns | |
| raw["cardiovascular"] += 35 * cardio | |
| if s.type == "sleep": | |
| brain_hit = SLEEP_BRAIN.get(s.sleep_hours or "4-6", 0.75) | |
| raw["brain"] += 35 * brain_hit | |
| raw["gut"] += 15 * brain_hit | |
| if s.type == "stress": | |
| carried = s.stress_carried != "mostly_gone" | |
| raw["brain"] += 28 if carried else 14 | |
| raw["cardiovascular"] += 15 if carried else 7 | |
| if s.type == "ill": | |
| sev_mod = 1.2 if s.ill_severity == "floored" else (0.6 if s.ill_severity == "mild" else 0.9) | |
| raw["gut"] += 30 * sev_mod | |
| raw["brain"] += 20 * sev_mod | |
| raw["muscular"] += 15 * sev_mod | |
| raw["cardiovascular"] += 12 * sev_mod | |
| if s.type == "care": | |
| raw["brain"] -= 8 | |
| raw["cardiovascular"] -= 8 | |
| raw["liver"] -= 5 | |
| raw["muscular"] -= 5 | |
| raw["gut"] -= 5 | |
| if bed_time and wake_time: | |
| penalty = circadian_penalty(bed_time, wake_time) | |
| raw["brain"] += penalty["brain_pts"] | |
| raw["cardiovascular"] += penalty["cardio_pts"] | |
| results = [] | |
| for system in RECOVERY_SYSTEMS: | |
| meta = SYSTEM_META[system] | |
| score = max(0, min(100, round(raw[system]))) | |
| recovery_hrs = (score / 100) * meta["base_window_hrs"] | |
| cleared_at = now + timedelta(hours=recovery_hrs) | |
| science = SCIENCE.get(system) | |
| results.append( | |
| SystemScore( | |
| system=system, | |
| label=meta["label"], | |
| icon=meta["icon"], | |
| score=score, | |
| cleared_at=cleared_at.strftime("%I:%M%p %A").lstrip("0"), | |
| recovery_hrs=round(recovery_hrs, 1), | |
| cause_text=_build_cause_text(system, stressors), | |
| action_text=_build_action_text(system, stressors), | |
| science_fact=science["fact"] if science else None, | |
| science_cite=science["cite"] if science else None, | |
| ) | |
| ) | |
| return results | |
| # βββ Circadian penalty ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _parse_hour(time_str: str) -> Optional[float]: | |
| import re | |
| clean = time_str.strip().upper() | |
| m = re.match(r"^(\d{1,2}):(\d{2})\s*(AM|PM)?$", clean) | |
| if not m: | |
| return None | |
| h = int(m.group(1)) | |
| mins = int(m.group(2)) | |
| period = m.group(3) | |
| if period == "PM" and h != 12: | |
| h += 12 | |
| if period == "AM" and h == 12: | |
| h = 0 | |
| return h + mins / 60 | |
| def circadian_penalty(bed_time: str, wake_time: str) -> dict: | |
| bed = _parse_hour(bed_time) | |
| wake = _parse_hour(wake_time) | |
| if bed is None or wake is None: | |
| return {"brain_pts": 0, "cardio_pts": 0, "label": "unknown"} | |
| sleep_hrs = (24 - bed) + wake if bed > wake else wake - bed | |
| brain_pts = 0 | |
| cardio_pts = 0 | |
| label = "aligned" | |
| if 0 <= bed < 2: | |
| brain_pts, cardio_pts, label = 10, 5, "mild misalignment" | |
| elif 2 <= bed < 4: | |
| brain_pts, cardio_pts, label = 22, 10, "significant misalignment" | |
| elif 4 <= bed < 6: | |
| brain_pts, cardio_pts, label = 32, 16, "severe misalignment" | |
| if 0 < sleep_hrs < 6: | |
| brain_pts += round((6 - sleep_hrs) * 4) | |
| return {"brain_pts": brain_pts, "cardio_pts": cardio_pts, "label": label} | |
| # βββ Helper text builders βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _build_cause_text(system: str, stressors: list[Stressor]) -> str: | |
| alcohol = next((s for s in stressors if s.type == "alcohol"), None) | |
| training = next((s for s in stressors if s.type == "training"), None) | |
| sleep = next((s for s in stressors if s.type == "sleep"), None) | |
| stress = next((s for s in stressors if s.type == "stress"), None) | |
| ill = next((s for s in stressors if s.type == "ill"), None) | |
| if system == "liver": | |
| if alcohol: | |
| t = (alcohol.alcohol_type or "alcohol").replace("_", " ") | |
| c = alcohol.alcohol_count or "several drinks" | |
| return f"{t.capitalize()} β {c} units to process" | |
| return "No significant liver load" | |
| if system == "brain": | |
| if alcohol and alcohol.alcohol_type in ("spirits", "cocktails"): | |
| return "Spirits/cocktails hit cognition hardest. Decision quality reduced." | |
| if sleep: | |
| return f"{(sleep.sleep_hours or 'Poor sleep').replace('_', ' ')} β cognitive recovery in progress" | |
| if stress and stress.stress_carried != "mostly_gone": | |
| return "Stress hormones still elevated. Focus window reduced." | |
| return "Mild cognitive load" | |
| if system == "cardiovascular": | |
| if training and training.training_area in ("hiit", "cardio"): | |
| return f"{training.training_area.upper()} session β heart rate recovery active" | |
| if alcohol: | |
| return "Alcohol elevates resting HR for 12β18hrs" | |
| return "Mild cardiovascular load" | |
| if system == "muscular": | |
| if training: | |
| area = (training.training_area or "training").replace("_", " ").capitalize() | |
| intensity = training.training_intensity or "hard" | |
| return f"{area} session at {intensity} intensity β CNS repair ongoing" | |
| return "No significant muscular load" | |
| if system == "gut": | |
| if alcohol and alcohol.alcohol_type == "beer": | |
| return "Beer β carbonation and fermentation byproducts affecting gut" | |
| if alcohol and alcohol.alcohol_type == "cocktails": | |
| return "Cocktail mixers adding fructose and gut load" | |
| if sleep: | |
| return "Poor sleep disrupts gut microbiome rhythm" | |
| if ill: | |
| return "Illness affecting gut barrier function" | |
| return "Minimal gut load" | |
| return "" | |
| def _build_action_text(system: str, stressors: list[Stressor]) -> str: | |
| alcohol = next((s for s in stressors if s.type == "alcohol"), None) | |
| training = next((s for s in stressors if s.type == "training"), None) | |
| if system == "liver": | |
| return ( | |
| "Avoid further alcohol. 500ml water + electrolytes now." | |
| if alcohol | |
| else "Liver clear β no action needed." | |
| ) | |
| if system == "brain": | |
| return "No decisions requiring deep focus until your window opens." | |
| if system == "cardiovascular": | |
| return ( | |
| "No cardio today. Walk only." | |
| if training and training.training_intensity == "destroyed" | |
| else "Keep activity light until cleared." | |
| ) | |
| if system == "muscular": | |
| return ( | |
| "Protein within 2 hrs. No re-training the same group today." | |
| if training | |
| else "No significant muscular debt." | |
| ) | |
| if system == "gut": | |
| return ( | |
| "Bland foods, no coffee on an empty stomach, no more alcohol." | |
| if alcohol | |
| else "Probiotic-rich foods will help speed gut clearance." | |
| ) | |
| return "" | |
| # βββ Counterfactual engine ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # | |
| # "If you had slept 7+ hours, Brain debt would drop from 67 to 22." | |
| # | |
| # The most leveraged single change to the user's stress profile. We find the | |
| # highest non-cleared system, identify the stressor contributing most to it, | |
| # and propose a single reversible flip (e.g. sleep 4-6 -> 6-7) that would | |
| # lower the score the most. Returned as a renderable sentence. | |
| COUNTERFACTUAL_FLIPS = { | |
| "sleep": { | |
| "field": "sleep_hours", | |
| "from_to": {"under_4": "6-7", "4-6": "6-7", "6-7": "6-7"}, | |
| "label": "slept 7+ hours", | |
| }, | |
| "training": { | |
| "field": "training_intensity", | |
| "from_to": {"destroyed": "easy", "hard": "easy", "easy": "easy"}, | |
| "label": "trained easy instead of hard", | |
| }, | |
| "alcohol": { | |
| "field": "alcohol_count", | |
| "from_to": {"lost_count": "1-2", "5+": "1-2", "3-4": "1-2", "1-2": "1-2"}, | |
| "label": "kept it to 1β2 drinks", | |
| }, | |
| "stress": { | |
| "field": "stress_carried", | |
| "from_to": {"yes": "mostly_gone", "mostly_gone": "mostly_gone"}, | |
| "label": "let the stress clear", | |
| }, | |
| "ill": { | |
| "field": "ill_severity", | |
| "from_to": {"floored": "mild", "moderate": "mild", "mild": "mild"}, | |
| "label": "caught the illness earlier", | |
| }, | |
| } | |
| SYSTEM_LABEL_NICE = { | |
| "cardiovascular": "Cardiovascular", | |
| "brain": "Brain", | |
| "liver": "Liver", | |
| "muscular": "Muscular / CNS", | |
| "gut": "Gut", | |
| } | |
| def compute_counterfactual( | |
| stressors: list, | |
| current_system_scores: list, | |
| bed_time: Optional[str] = None, | |
| wake_time: Optional[str] = None, | |
| ) -> Optional[dict]: | |
| """Return the single highest-leverage change the user could make. | |
| Iterates over every stressor Γ every possible flip and returns the | |
| flip that lowers the target (worst non-cleared) system the most. | |
| Returns a dict {system, from_score, to_score, drop, lever_label} or None | |
| if no clear lever exists. | |
| """ | |
| ranked = sorted(current_system_scores, key=lambda s: -s.score) | |
| target = next((s for s in ranked if s.score > 20), None) | |
| if not target: | |
| return None | |
| best: Optional[dict] = None | |
| for s in stressors: | |
| if s.type not in COUNTERFACTUAL_FLIPS: | |
| continue | |
| flip = COUNTERFACTUAL_FLIPS[s.type] | |
| field = flip["field"] | |
| current_val = getattr(s, field, None) | |
| if current_val is None: | |
| continue | |
| target_val = flip["from_to"].get(current_val) | |
| if target_val is None or target_val == current_val: | |
| continue | |
| modified = [] | |
| for s2 in stressors: | |
| if s2 is s: | |
| modified.append(Stressor(**{**s2.__dict__, field: target_val})) | |
| else: | |
| modified.append(s2) | |
| new_scores = compute_system_scores( | |
| modified, | |
| now=datetime.now(), | |
| bed_time=bed_time, | |
| wake_time=wake_time, | |
| ) | |
| new_target = next((x for x in new_scores if x.system == target.system), None) | |
| if new_target is None: | |
| continue | |
| drop = target.score - new_target.score | |
| if drop <= 0: | |
| continue | |
| candidate = { | |
| "system": target.system, | |
| "system_label": SYSTEM_LABEL_NICE.get(target.system, target.system), | |
| "from_score": target.score, | |
| "to_score": new_target.score, | |
| "drop": drop, | |
| "lever_label": flip["label"], | |
| } | |
| if best is None or candidate["drop"] > best["drop"]: | |
| best = candidate | |
| return best | |