File size: 11,400 Bytes
4eefabb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
"""
Central configuration for MicroClimate-X.

EVERY Veto threshold below has an academic / regulatory citation.
This is intentional โ€” at thesis defence the panel WILL ask
"why 3500 m, why -5 ยฐC, why 40 km/h?". Be ready to point to this file.
"""
from __future__ import annotations

import os
import subprocess
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
MODEL_DIR = ROOT / "models"
DATA_DIR  = ROOT / "data"
DB_PATH   = Path(os.environ.get("MICROCLIMATEX_DB", str(ROOT / "cache.sqlite3")))


def _detect_git_revision() -> str:
    """Best-effort short SHA. Returns "unknown" if git is not available
    or this directory isn't a checkout (e.g. inside a Docker image)."""
    env = os.environ.get("MICROCLIMATEX_GIT_REV")
    if env:
        return env
    try:
        out = subprocess.run(
            ["git", "rev-parse", "--short", "HEAD"],
            cwd=ROOT, capture_output=True, text=True, timeout=2.0,
        )
        if out.returncode == 0:
            return out.stdout.strip()
    except (FileNotFoundError, subprocess.SubprocessError):    # pragma: no cover
        pass
    return "unknown"


GIT_REVISION = _detect_git_revision()


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Veto thresholds โ€” one-vote rejection rules
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Citation: Luks et al. (2019) "Wilderness Medical Society Practice
#           Guidelines for the Prevention and Treatment of Acute Altitude
#           Illness." High altitude (>2500 m) carries clinical risk; severe
#           hypoxia onset is well-documented above ~3500 m.
ALTITUDE_HYPOXIA_M = 3500.0

# Citation: WMO Beaufort scale โ€” Force 6 "Strong breeze" โ‰ˆ 39-49 km/h,
#           the threshold above which outdoor activity becomes hazardous.
GALE_WIND_KMH = 40.0

# Citation: UIAA Medical Commission frostbite risk guidance โ€” exposed skin
#           freezes rapidly below approximately -5 ยฐC with wind chill.
EXTREME_COLD_C = -5.0

# Citation: U.S. NWS convective forecasting handbook โ€” CAPE > 1000 J/kg
#           indicates moderate-to-strong instability suitable for
#           thunderstorm development.
HIGH_CAPE_JKG = 1000.0

# Citation: FAA AIM 7-1-12 โ€” visibility below 100 m is classified as
#           Category III instrument-only conditions. Used here as an extreme
#           low-visibility threshold (whiteout / dense fog).
LOW_VISIBILITY_M = 100.0

# Wind alignment with slope normal vector (orographic uplift). The
# threshold 0.7 corresponds to ~45 degrees of slope-facing wind.
OROGRAPHIC_DOT_THRESHOLD = 0.7

# Wet-flood trigger in a valley basin: high probability of localised rain
# combined with valley-floor topography.
VALLEY_FLOOD_PROB = 0.80


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Risk scoring (additive penalties when no Veto fires)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
PENALTY = {
    "ml_high_rain_prob": 35,   # ML predicts >= 70 % rain probability
    "ml_mid_rain_prob":  15,   # ML predicts 40-70 % rain probability
    "valley_floor":      10,
    "windward_slope":    20,
    "orographic_lift":   25,
    "altitude_high":     15,   # 2500-3500 m, sub-Veto altitude band
    "wind_strong":       10,   # 25-40 km/h
}


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Four hazard categories โ€” matches D5 proposal ยง3.7 / P4.3
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Fog risk:
#   WMO surface synoptic code: fog โ‰ˆ visibility < 1 km, RH typically > 95 %,
#   dew-point depression < ~2 ยฐC. Valley/Slope basins trap radiation fog.
FOG_HUMIDITY_PCT      = 95.0
FOG_DEW_DEP_MAX_C     = 2.0
FOG_CLOUD_BASE_MAX_M  = 800.0    # from D5 ยง3.7.2 decision table

# Wind gust risk:
#   On exposed ridges and mountain passes, sustained 25 km/h winds with
#   topographic acceleration commonly gust to Beaufort F6 levels.
GUST_WIND_MIN_KMH     = 25.0     # below GALE_WIND_KMH but still risky

# Thunderstorm risk:
#   NWS "moderate instability" begins at CAPE 500 J/kg; sharp pressure drop
#   often precedes convective initiation.
THUNDER_CAPE_MIN_JKG  = 500.0
THUNDER_PRESSURE_DROP = -2.0     # hPa over past 3 h (matches D5 ยง1.3 example)


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Decision Table โ€” D5 ยง3.7.2 / Table 4.2  (one-to-one with the thesis)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Each rule fires when ALL of its non-None conditions hold. The thesis
# narrative motivates this table as: "macro forecast says no rain, but
# the local terrain conditions imply hidden risk".
DECISION_TABLE_3_7_2 = {
    "R1": {
        "description":            "Hidden rain risk โ€” macro says no, terrain says yes",
        "macro_rain_prob_max":    0.30,
        "macro_rain_prob_min":    None,
        "humidity_min_pct":       85.0,
        "wind_into_slope":        True,
        "terrain":                "WindwardSlope",
        "pressure_change_3h_max": -1.5,
        "cloud_base_max_m":       FOG_CLOUD_BASE_MAX_M,
        "conclusion_en":          "Hidden rain risk: terrain analysis indicates orographic precipitation despite low macro probability.",
        "conclusion_zh":          "้š่—้™้›จ้ฃŽ้™ฉ๏ผšๅฎ่ง‚้ข„ๆŠฅๆฆ‚็އไฝŽ๏ผŒไฝ†ๅœฐๅฝขๅˆ†ๆž่กจๆ˜Žๅญ˜ๅœจๅœฐๅฝขๆŠฌๅ‡้™ๆฐดใ€‚",
    },
    "R2": {
        "description":            "No significant risk โ€” terrain not aligned",
        "macro_rain_prob_max":    0.30,
        "macro_rain_prob_min":    None,
        "humidity_min_pct":       85.0,
        "wind_into_slope":        False,
        "terrain":                "LeewardOrValley",
        "pressure_change_3h_max": -1.5,
        "cloud_base_max_m":       FOG_CLOUD_BASE_MAX_M,
        "conclusion_en":          "No significant rainfall danger at this spot in this period.",
        "conclusion_zh":          "ๆญคๅœฐๆญคๆ—ถๆ— ๆ˜พ่‘—้™้›จๅฑ้™ฉใ€‚",
    },
    "R3": {
        "description":            "Heavy downpour incoming โ€” avoid exposure",
        "macro_rain_prob_max":    None,
        "macro_rain_prob_min":    0.70,
        "humidity_min_pct":       None,
        "wind_into_slope":        True,
        "terrain":                "WindwardSlope",
        "pressure_change_3h_max": None,
        "cloud_base_max_m":       None,
        "conclusion_en":          "Heavy downpour incoming. Avoid mountains and valleys.",
        "conclusion_zh":          "ๅผบ้™้›จๅณๅฐ†ๅˆฐๆฅใ€‚่ฏท้ฟๅผ€ๅฑฑๅŒบไธŽๅณก่ฐทใ€‚",
    },
    "R4": {
        "description":            "Normal rain โ€” no terrain amplification",
        "macro_rain_prob_max":    None,
        "macro_rain_prob_min":    0.70,
        "humidity_min_pct":       None,
        "wind_into_slope":        None,
        "terrain":                None,
        "pressure_change_3h_max": None,
        "cloud_base_max_m":       None,
        "conclusion_en":          "Rain expected, but no terrain-induced amplification. Standard rain precautions apply.",
        "conclusion_zh":          "้ข„่ฎกๆœ‰้›จ๏ผŒไฝ†ๆ— ๅœฐๅฝขๆŠฌๅ‡ๆ”พๅคงใ€‚ๆŒ‰ไธ€่ˆฌ้›จๅคฉๆŽชๆ–ฝๅบ”ๅฏนๅณๅฏใ€‚",
    },
}


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Activity-aware weighting โ€” D5 ยง3.7 / P4.4
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Composite = ฮฃ w_i ยท subscore_i, then renormalised to 0-100.
# Rows: activity. Cols: rainfall, fog, wind_gust, thunderstorm.
ACTIVITY_WEIGHTS = {
    "hiker":        {"rainfall": 1.0, "fog": 1.3, "wind_gust": 1.0, "thunderstorm": 1.4},
    "driver":       {"rainfall": 0.8, "fog": 1.5, "wind_gust": 1.3, "thunderstorm": 0.9},
    "construction": {"rainfall": 1.0, "fog": 0.8, "wind_gust": 1.5, "thunderstorm": 1.4},
    "general":      {"rainfall": 1.0, "fog": 1.0, "wind_gust": 1.0, "thunderstorm": 1.0},
}


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Cache TTL (risk-adaptive)
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Safety-critical apps must not serve stale "Safe" verdicts during developing
# storms. Bucket TTL by risk band.
TTL_HIGH_RISK_SEC = 60      # any Veto fired OR risk >= 70
TTL_MID_RISK_SEC  = 300     # risk 40-70
TTL_LOW_RISK_SEC  = 600     # risk < 40

# Grid resolution used as cache key (0.01ยฐ โ‰ˆ 1.1 km at the equator).
GRID_RESOLUTION_DEG = 0.01


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# External API endpoints
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
OPEN_METEO_FORECAST_URL = "https://api.open-meteo.com/v1/forecast"
OPEN_TOPO_URL           = "https://api.opentopodata.org/v1/srtm30m"


# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Domain constants
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# WMO definition of trace precipitation.
RAIN_THRESHOLD_MM = 0.1