44"""
55
66import math
7- from datetime import datetime , timedelta
7+ from datetime import date , datetime , timedelta
88from pathlib import Path
99
1010import pandas as pd
1616LONGITUDE = 39.18
1717SOLAR_CONSTANT = 1361 # W/m2
1818
19- # ── Beaufort Scale ────────────────────────────────────────────────────────────
19+ # ── Beaufort Scale (WMO definition, classified in knots) ──────────────────────
20+ # The Beaufort scale is defined in knots. All other units are derived using the
21+ # exact conversion factors below to avoid boundary errors.
22+ #
23+ # Exact conversion factors (from 1 nautical mile = 1852 m exactly):
24+ # 1 kn = 463/250 km/h (= 1.852 km/h, exact rational value)
25+ # 1 kn = 463/900 m/s (= 0.51444... m/s, exact rational value)
26+ KN_TO_KPH = 463 / 250 # km/h per knot
27+ KN_TO_MS = 463 / 900 # m/s per knot
28+ KPH_TO_KN = 250 / 463 # knots per km/h
29+ MS_TO_KN = 900 / 463 # knots per m/s
30+
31+ # Thresholds in knots (WMO integer boundaries). range_kn is [lo, hi).
2032BEAUFORT_SCALE = {
21- 0 : {"range" : (0 , 1 ), "label" : "Calm" },
22- 1 : {"range" : (1 , 6 ), "label" : "Light air" },
23- 2 : {"range" : (6 , 12 ), "label" : "Light breeze" },
24- 3 : {"range" : (12 , 20 ), "label" : "Gentle breeze" },
25- 4 : {"range" : (20 , 29 ), "label" : "Moderate breeze" },
26- 5 : {"range" : (29 , 39 ), "label" : "Fresh breeze" },
27- 6 : {"range" : (39 , 50 ), "label" : "Strong breeze" },
28- 7 : {"range" : (50 , 62 ), "label" : "Near gale" },
29- 8 : {"range" : (62 , 75 ), "label" : "Gale" },
30- 9 : {"range" : (75 , 89 ), "label" : "Strong gale" },
31- 10 : {"range" : (89 , 103 ), "label" : "Storm" },
32- 11 : {"range" : (103 , 118 ), "label" : "Violent storm" },
33- 12 : {"range" : (118 , 999 ), "label" : "Hurricane" },
33+ 0 : {"range_kn" : (0 , 1 ), "label" : "Calm" },
34+ 1 : {"range_kn" : (1 , 4 ), "label" : "Light air" },
35+ 2 : {"range_kn" : (4 , 7 ), "label" : "Light breeze" },
36+ 3 : {"range_kn" : (7 , 11 ), "label" : "Gentle breeze" },
37+ 4 : {"range_kn" : (11 , 17 ), "label" : "Moderate breeze" },
38+ 5 : {"range_kn" : (17 , 22 ), "label" : "Fresh breeze" },
39+ 6 : {"range_kn" : (22 , 28 ), "label" : "Strong breeze" },
40+ 7 : {"range_kn" : (28 , 34 ), "label" : "Near gale" },
41+ 8 : {"range_kn" : (34 , 41 ), "label" : "Gale" },
42+ 9 : {"range_kn" : (41 , 48 ), "label" : "Strong gale" },
43+ 10 : {"range_kn" : (48 , 56 ), "label" : "Storm" },
44+ 11 : {"range_kn" : (56 , 64 ), "label" : "Violent storm" },
45+ 12 : {"range_kn" : (64 , 999 ), "label" : "Hurricane force" },
46+ }
47+
48+ # ── Wind Classification Systems ─────────────────────────────────────────────
49+ # All systems defined in their original published units.
50+ # Use the conversion constants above to convert to the working unit at runtime.
51+ #
52+ # native_unit: 'kn' = knots, 'ms' = m/s, 'computed' = derived from dataset.
53+ # bands: list of {label, lo, hi} where hi=None means no upper limit.
54+ WIND_CLASSIFICATIONS = {
55+ "beaufort" : {
56+ "label" : "Beaufort" ,
57+ "native_unit" : "kn" ,
58+ "source" : "WMO/Beaufort 1805" ,
59+ "notes" : "Sea-level scale; Bf 9-12 merged into Severe+ for pedestrian relevance. Force numbers available as metadata." ,
60+ "bands" : [
61+ {"label" : "Calm" , "lo" : 0 , "hi" : 1 , "force" : 0 },
62+ {"label" : "Light Air" , "lo" : 1 , "hi" : 4 , "force" : 1 },
63+ {"label" : "Light Breeze" , "lo" : 4 , "hi" : 7 , "force" : 2 },
64+ {"label" : "Gentle Breeze" , "lo" : 7 , "hi" : 11 , "force" : 3 },
65+ {"label" : "Moderate Breeze" , "lo" : 11 , "hi" : 17 , "force" : 4 },
66+ {"label" : "Fresh Breeze" , "lo" : 17 , "hi" : 22 , "force" : 5 },
67+ {"label" : "Strong Breeze" , "lo" : 22 , "hi" : 28 , "force" : 6 },
68+ {"label" : "Near Gale" , "lo" : 28 , "hi" : 34 , "force" : 7 },
69+ {"label" : "Gale" , "lo" : 34 , "hi" : 41 , "force" : 8 },
70+ {"label" : "Severe+" , "lo" : 41 , "hi" : None , "force" : "9+" },
71+ ],
72+ },
73+ "lawson" : {
74+ "label" : "Lawson 2001" ,
75+ "native_unit" : "ms" ,
76+ "source" : "Lawson T.V., 2001, pedestrian comfort criteria, assessed at 1.5m height" ,
77+ "notes" : "Upper-bound thresholds. Use this version, not LDDC." ,
78+ "bands" : [
79+ {"label" : "Sitting" , "lo" : 0 , "hi" : 4 },
80+ {"label" : "Standing" , "lo" : 4 , "hi" : 6 },
81+ {"label" : "Strolling" , "lo" : 6 , "hi" : 8 },
82+ {"label" : "Business Walking" , "lo" : 8 , "hi" : 10 },
83+ {"label" : "Uncomfortable" , "lo" : 10 , "hi" : None },
84+ ],
85+ },
86+ "davenport" : {
87+ "label" : "Davenport" ,
88+ "native_unit" : "ms" ,
89+ "source" : "Davenport A.G., 1975, first published pedestrian wind comfort criterion, exceedance probability 1.5%" ,
90+ "notes" : "Upper-bound thresholds." ,
91+ "bands" : [
92+ {"label" : "Long Sitting" , "lo" : 0 , "hi" : 3.6 },
93+ {"label" : "Short Sitting" , "lo" : 3.6 , "hi" : 5.3 },
94+ {"label" : "Walking Quietly" , "lo" : 5.3 , "hi" : 7.6 },
95+ {"label" : "Walking Fast" , "lo" : 7.6 , "hi" : 9.8 },
96+ {"label" : "Uncomfortable" , "lo" : 9.8 , "hi" : None },
97+ ],
98+ },
99+ "percentile" : {
100+ "label" : "Percentiles" ,
101+ "native_unit" : "computed" ,
102+ "source" : "WMO climatological quintile practice" ,
103+ "notes" : "Boundaries computed at runtime from the full available dataset. Quintile-based: P20, P50, P80, P95." ,
104+ "bands" : [
105+ {"label" : "Calm" , "percentile_hi" : 20 },
106+ {"label" : "Light" , "percentile_lo" : 20 , "percentile_hi" : 50 },
107+ {"label" : "Moderate" , "percentile_lo" : 50 , "percentile_hi" : 80 },
108+ {"label" : "Strong" , "percentile_lo" : 80 , "percentile_hi" : 95 },
109+ {"label" : "Exceptional" , "percentile_lo" : 95 },
110+ ],
111+ },
34112}
35113
36114# ── 16-point Compass ─────────────────────────────────────────────────────────
@@ -228,10 +306,15 @@ def compass_bin(degrees, n_points=16):
228306
229307
230308def beaufort_number (speed_kph ):
231- """Return the Beaufort number for a given wind speed in km/h."""
309+ """Return the Beaufort number for a given wind speed in km/h.
310+
311+ Classification is performed in knots (WMO definition) to avoid
312+ boundary errors from rounded secondary-unit thresholds.
313+ """
314+ speed_kn = speed_kph * KPH_TO_KN
232315 for num , info in BEAUFORT_SCALE .items ():
233- lo , hi = info ["range " ]
234- if lo <= speed_kph < hi :
316+ lo , hi = info ["range_kn " ]
317+ if lo <= speed_kn < hi :
235318 return num
236319 return 12
237320
@@ -338,8 +421,11 @@ def build_available_periods(df):
338421 key = (d .year , s_idx )
339422 if key not in seen_seasons :
340423 seen_seasons .add (key )
424+ _MONTH_ABBR = ['Jan' ,'Feb' ,'Mar' ,'Apr' ,'May' ,'Jun' ,'Jul' ,'Aug' ,'Sep' ,'Oct' ,'Nov' ,'Dec' ]
425+ m_first = _MONTH_ABBR [SEASONS [s_idx ]['months' ][0 ] - 1 ]
426+ m_last = _MONTH_ABBR [SEASONS [s_idx ]['months' ][- 1 ] - 1 ]
341427 seasons .append ({
342- "label" : f"{ s_name } ({ SEASONS [ s_idx ][ 'months' ][ 0 ] } - { SEASONS [ s_idx ][ 'months' ][ - 1 ] } ) { d .year } " ,
428+ "label" : f"{ s_name } ({ m_first } \u2013 { m_last } ) { d .year } " ,
343429 "year" : d .year , "season" : s_idx
344430 })
345431
@@ -352,7 +438,7 @@ def build_available_periods(df):
352438 if key not in seen_weeks :
353439 seen_weeks .add (key )
354440 weeks .append ({
355- "label" : f"Week { iso_week } , { iso_year } " ,
441+ "label" : "W/s " + date . fromisocalendar ( iso_year , iso_week , 1 ). strftime ( "%d/%m/%y" ) ,
356442 "year" : iso_year , "week" : iso_week
357443 })
358444
0 commit comments