Skip to content

Commit 7dd9fb2

Browse files
sync embedded/arc_tz_weather from arc_tz_weather
1 parent 8dcbf00 commit 7dd9fb2

5 files changed

Lines changed: 297 additions & 58 deletions

File tree

embedded/arc_tz_weather/index.html

Lines changed: 261 additions & 32 deletions
Large diffs are not rendered by default.

embedded/arc_tz_weather/modules/common.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
KPH_TO_KN = 250 / 463 # knots per km/h
2929
MS_TO_KN = 900 / 463 # knots per m/s
3030

31+
# Calm threshold: 0.1 m/s expressed in km/h (0.1 * 3.6 = 0.36)
32+
CALM_THRESHOLD_KPH = 0.36
33+
3134
# Thresholds in knots (WMO integer boundaries). range_kn is [lo, hi).
3235
BEAUFORT_SCALE = {
3336
0: {"range_kn": (0, 1), "label": "Calm"},
@@ -368,6 +371,14 @@ def get_season(month):
368371
return "Unknown"
369372

370373

374+
_SEASON_BOUNDARY_LABELS = {
375+
1: "January Dry Season (Kiangazi)",
376+
3: "Long Rains (Masika)",
377+
6: "June Dry Season (Kiangazi)",
378+
11: "Short Rains (Vuli)",
379+
}
380+
381+
371382
def get_season_boundaries(df):
372383
"""Get season boundary timestamps for marking on charts."""
373384
boundaries = []
@@ -383,8 +394,7 @@ def get_season_boundaries(df):
383394
for m in season_starts:
384395
dt = TIMEZONE.localize(datetime(year, m, 1))
385396
if min_date <= dt <= max_date:
386-
season_name = get_season(m)
387-
boundaries.append({"ts": to_eat_ms(dt), "label": season_name})
397+
boundaries.append({"ts": to_eat_ms(dt), "label": _SEASON_BOUNDARY_LABELS[m]})
388398
year += 1
389399
return boundaries
390400

embedded/arc_tz_weather/modules/cross_variable.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import numpy as np
1111

1212
from .common import (
13-
VENTILATION_COLORS, to_eat_ms, compass_bin,
13+
VENTILATION_COLORS, CALM_THRESHOLD_KPH, to_eat_ms, compass_bin,
1414
get_season_boundaries,
1515
)
1616

@@ -59,7 +59,7 @@ def process(df, rain_events=None):
5959
def _build_driving_rain_index(xdf):
6060
"""Build Driving Rain Index polar chart and time series."""
6161
# Only readings with both wind and rain
62-
wr = xdf[(xdf["avg_wind_kph"] > 0) & (xdf["precip_rate_mmh"] > 0)].copy()
62+
wr = xdf[(xdf["avg_wind_kph"] > CALM_THRESHOLD_KPH) & (xdf["precip_rate_mmh"] > 0)].copy()
6363

6464
if len(wr) == 0:
6565
return {"id": "driving-rain", "title": "Driving Rain Index",

embedded/arc_tz_weather/modules/precipitation.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -301,14 +301,14 @@ def _build_intensity_distribution(pdf):
301301

302302

303303
def _build_diurnal_rainfall(pdf):
304-
"""Build diurnal rainfall pattern: mean hourly rainfall + rain probability."""
304+
"""Build diurnal rainfall pattern: mean hourly rainfall + rain frequency."""
305305
pdf_c = pdf.copy()
306306
pdf_c["hour"] = pdf_c["timestamp"].dt.hour
307307

308308
# Mean rainfall per hour (from incremental)
309309
hourly_rain = pdf_c.groupby("hour")["precip_incr"].mean()
310310

311-
# Rain probability by hour
311+
# Rain frequency by hour
312312
hourly_prob = pdf_c.groupby("hour").apply(
313313
lambda g: (g["precip_rate_mmh"] > 0).sum() / len(g) * 100
314314
)
@@ -328,7 +328,7 @@ def _build_diurnal_rainfall(pdf):
328328
{
329329
"type": "scatter",
330330
"mode": "lines+markers",
331-
"name": "Rain Probability (%)",
331+
"name": "Rain Frequency (%)",
332332
"x": hours,
333333
"y": rain_probs,
334334
"yaxis": "y2",
@@ -341,7 +341,7 @@ def _build_diurnal_rainfall(pdf):
341341
"xaxis": {"title": "Hour of Day (EAT)", "dtick": 1},
342342
"yaxis": {"title": "Mean Rainfall (mm)"},
343343
"yaxis2": {
344-
"title": "Rain Probability (%)",
344+
"title": "Rain Frequency (%)",
345345
"overlaying": "y",
346346
"side": "right",
347347
"range": [0, max(rain_probs) * 1.2] if rain_probs else [0, 100],

embedded/arc_tz_weather/modules/wind.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from .common import (
1313
COMPASS_DIRS_16, COMPASS_DIRS_8, WIND_SPEED_BINS, WIND_SPEED_LABELS,
1414
WIND_SPEED_COLORS, BEAUFORT_SCALE, VENTILATION_COLORS, WIND_CLASSIFICATIONS,
15-
KN_TO_KPH, TIMEZONE,
15+
KN_TO_KPH, TIMEZONE, CALM_THRESHOLD_KPH,
1616
spike_filter, compass_bin, beaufort_number, weibull_fit, to_eat_ms,
1717
get_season_boundaries,
1818
)
@@ -40,10 +40,10 @@ def process(df):
4040

4141
# ── Summary Statistics ────────────────────────────────────────────────
4242
total_readings = len(wdf)
43-
calm_readings = (wdf["avg_wind_kph"] == 0).sum()
43+
calm_readings = (wdf["avg_wind_kph"] <= CALM_THRESHOLD_KPH).sum()
4444
calm_pct = round(calm_readings / total_readings * 100, 1) if total_readings else 0
4545

46-
non_calm = wdf[wdf["avg_wind_kph"] > 0]
46+
non_calm = wdf[wdf["avg_wind_kph"] > CALM_THRESHOLD_KPH]
4747
mean_speed = round(wdf["avg_wind_kph"].mean(), 1)
4848
mean_speed_noncalm = round(non_calm["avg_wind_kph"].mean(), 1) if len(non_calm) else 0
4949
max_speed = round(wdf["avg_wind_kph"].max(), 1)
@@ -108,9 +108,9 @@ def _build_wind_rose(wdf, n_points):
108108
dir_labels = [d[0] for d in dirs]
109109
col = "compass_16" if n_points == 16 else "compass_8"
110110

111-
non_calm = wdf[wdf["avg_wind_kph"] > 0].copy()
111+
non_calm = wdf[wdf["avg_wind_kph"] > CALM_THRESHOLD_KPH].copy()
112112
total = len(wdf)
113-
calm_pct = round((wdf["avg_wind_kph"] == 0).sum() / total * 100, 1) if total else 0
113+
calm_pct = round((wdf["avg_wind_kph"] <= CALM_THRESHOLD_KPH).sum() / total * 100, 1) if total else 0
114114

115115
# Speed bins: 0-5, 5-10, 10-15, 15-20, 20+
116116
traces = []
@@ -176,9 +176,9 @@ def _build_wind_timeseries(wdf):
176176
"""Build wind speed time series with average and gust."""
177177
timestamps = [to_eat_ms(t) for t in wdf["timestamp"]]
178178

179-
# 24-hour running mean
179+
# 12-hour running mean
180180
wdf_sorted = wdf.set_index("timestamp").sort_index()
181-
running_mean = wdf_sorted["avg_wind_kph"].rolling("24h", min_periods=1).mean()
181+
running_mean = wdf_sorted["avg_wind_kph"].rolling("12h", center=True, min_periods=1).mean()
182182
rm_ts = [to_eat_ms(t) for t in running_mean.index]
183183
rm_vals = [round(v, 1) if not pd.isna(v) else None for v in running_mean.values]
184184

@@ -208,7 +208,7 @@ def _build_wind_timeseries(wdf):
208208
{
209209
"type": "scatter",
210210
"mode": "lines",
211-
"name": "24h Mean",
211+
"name": "12h Mean",
212212
"x_ms": rm_ts,
213213
"y": rm_vals,
214214
"line": {"color": "#d62728", "width": 2},
@@ -242,7 +242,7 @@ def _build_diurnal_wind(wdf):
242242

243243
# Calm percentage by hour
244244
calm_by_hour = wdf_c.groupby("hour").apply(
245-
lambda g: (g["avg_wind_kph"] == 0).sum() / len(g) * 100
245+
lambda g: (g["avg_wind_kph"] <= CALM_THRESHOLD_KPH).sum() / len(g) * 100
246246
).round(1)
247247

248248
# Optional: separate by month
@@ -293,7 +293,7 @@ def _build_diurnal_wind(wdf):
293293
},
294294
{
295295
"type": "bar",
296-
"name": "Calm %",
296+
"name": "Calm % (\u22640.1 m/s)",
297297
"x": hours,
298298
"y": calm_pcts,
299299
"yaxis": "y2",
@@ -334,7 +334,7 @@ def _build_wind_distribution(wdf, k_val, c_val):
334334
hist, bin_edges = np.histogram(speeds, bins=bins)
335335

336336
# Separate calm bar
337-
calm_count = int((speeds == 0).sum())
337+
calm_count = int((speeds <= CALM_THRESHOLD_KPH).sum())
338338
bin_centers = [(bin_edges[i] + bin_edges[i + 1]) / 2 for i in range(len(hist))]
339339

340340
traces = [{
@@ -387,7 +387,7 @@ def _build_wind_distribution(wdf, k_val, c_val):
387387

388388
def _build_gust_factor(wdf):
389389
"""Build gust factor scatter plot (gust factor vs avg speed, colored by hour)."""
390-
valid = wdf[(wdf["avg_wind_kph"] > 0) & wdf["peak_wind_kph"].notna()].copy()
390+
valid = wdf[(wdf["avg_wind_kph"] > CALM_THRESHOLD_KPH) & wdf["peak_wind_kph"].notna()].copy()
391391
valid["gust_factor"] = valid["peak_wind_kph"] / valid["avg_wind_kph"]
392392
valid["hour"] = valid["timestamp"].dt.hour
393393

@@ -440,7 +440,7 @@ def _build_gust_factor(wdf):
440440

441441
def _build_calm_periods(wdf):
442442
"""Build calm period analysis: distribution of consecutive calm durations."""
443-
is_calm = (wdf["avg_wind_kph"] == 0).values
443+
is_calm = (wdf["avg_wind_kph"] <= CALM_THRESHOLD_KPH).values
444444
timestamps = wdf["timestamp"].values
445445

446446
# Find consecutive calm periods
@@ -528,8 +528,8 @@ def _build_ventilation_availability(wdf):
528528
for date, group in wdf_c.groupby("date"):
529529
total = len(group)
530530
effective = (group["avg_wind_kph"] >= thresh).sum()
531-
marginal = ((group["avg_wind_kph"] > 0) & (group["avg_wind_kph"] < thresh)).sum()
532-
calm = (group["avg_wind_kph"] == 0).sum()
531+
marginal = ((group["avg_wind_kph"] > CALM_THRESHOLD_KPH) & (group["avg_wind_kph"] < thresh)).sum()
532+
calm = (group["avg_wind_kph"] <= CALM_THRESHOLD_KPH).sum()
533533

534534
# Convert to hours (assuming ~5-min intervals)
535535
interval_h = 5 / 60
@@ -545,8 +545,8 @@ def _build_ventilation_availability(wdf):
545545
# Default threshold stats
546546
default_thresh = 3.5
547547
all_effective = (wdf_c["avg_wind_kph"] >= default_thresh).sum()
548-
all_marginal = ((wdf_c["avg_wind_kph"] > 0) & (wdf_c["avg_wind_kph"] < default_thresh)).sum()
549-
all_calm = (wdf_c["avg_wind_kph"] == 0).sum()
548+
all_marginal = ((wdf_c["avg_wind_kph"] > CALM_THRESHOLD_KPH) & (wdf_c["avg_wind_kph"] < default_thresh)).sum()
549+
all_calm = (wdf_c["avg_wind_kph"] <= CALM_THRESHOLD_KPH).sum()
550550
total = len(wdf_c)
551551
eff_pct = round(all_effective / total * 100, 1) if total else 0
552552

@@ -583,7 +583,7 @@ def _build_ventilation_availability(wdf):
583583
{
584584
"type": "scatter",
585585
"mode": "lines",
586-
"name": "Calm",
586+
"name": "Calm (\u22640.1 m/s)",
587587
"x_ms": dates_ms,
588588
"y": calm_hours,
589589
"fill": "tonexty",

0 commit comments

Comments
 (0)