fm35-decoder 0.1.2__tar.gz → 1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. fm35_decoder-1.1/.gitignore +32 -0
  2. fm35_decoder-1.1/PKG-INFO +171 -0
  3. fm35_decoder-1.1/README.md +153 -0
  4. {fm35_decoder-0.1.2 → fm35_decoder-1.1}/pyproject.toml +1 -1
  5. fm35_decoder-1.1/src/fm35_decoder/__init__.py +3 -0
  6. fm35_decoder-1.1/src/fm35_decoder/decoder.py +1089 -0
  7. {fm35_decoder-0.1.2 → fm35_decoder-1.1}/src/fm35_decoder/table_codes/Sr_3849.csv +3 -0
  8. fm35_decoder-1.1/src/fm35_decoder/table_codes/sasa_3872.csv +27 -0
  9. fm35_decoder-1.1/src/fm35_decoder/tables.py +291 -0
  10. fm35_decoder-0.1.2/PKG-INFO +0 -106
  11. fm35_decoder-0.1.2/README.md +0 -88
  12. fm35_decoder-0.1.2/src/fm35_decoder/__init__.py +0 -3
  13. fm35_decoder-0.1.2/src/fm35_decoder/decoder.py +0 -1178
  14. fm35_decoder-0.1.2/src/fm35_decoder/table_codes/sasa_3872.csv +0 -11
  15. {fm35_decoder-0.1.2 → fm35_decoder-1.1}/LICENSE +0 -0
  16. {fm35_decoder-0.1.2 → fm35_decoder-1.1}/src/fm35_decoder/table_codes/CH_0509.csv +0 -0
  17. {fm35_decoder-0.1.2 → fm35_decoder-1.1}/src/fm35_decoder/table_codes/CL_0513.csv +0 -0
  18. {fm35_decoder-0.1.2 → fm35_decoder-1.1}/src/fm35_decoder/table_codes/CM_0515.csv +0 -0
  19. {fm35_decoder-0.1.2 → fm35_decoder-1.1}/src/fm35_decoder/table_codes/D_0777.csv +0 -0
  20. {fm35_decoder-0.1.2 → fm35_decoder-1.1}/src/fm35_decoder/table_codes/Nh_2700.csv +0 -0
  21. {fm35_decoder-0.1.2 → fm35_decoder-1.1}/src/fm35_decoder/table_codes/T_3931.csv +0 -0
  22. {fm35_decoder-0.1.2 → fm35_decoder-1.1}/src/fm35_decoder/table_codes/h_1600.csv +0 -0
  23. {fm35_decoder-0.1.2 → fm35_decoder-1.1}/src/fm35_decoder/table_codes/rara_3685.csv +0 -0
@@ -0,0 +1,32 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ *.egg
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # Testing and caching
18
+ .pytest_cache/
19
+ .mypy_cache/
20
+ .ruff_cache/
21
+ .coverage
22
+ htmlcov/
23
+
24
+ # Editor / IDE
25
+ .vscode/
26
+ .idea/
27
+ *.swp
28
+ *.swo
29
+
30
+ # OS files
31
+ .DS_Store
32
+ Thumbs.db
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.5
2
+ Name: fm35_decoder
3
+ Version: 1.1
4
+ Summary: A Python tool to decode standard FM35 TEMP weather messages from upper-air observations.
5
+ Project-URL: Homepage, https://github.com/luciodaou/fm35_decoder
6
+ Project-URL: Bug Tracker, https://github.com/luciodaou/fm35_decoder/issues
7
+ Author: Lucio Daou
8
+ License-File: LICENSE
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.12
13
+ Requires-Dist: numpy>=2.0.0
14
+ Requires-Dist: pandas>=3.0.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest; extra == 'dev'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # FM35 Decoder
20
+
21
+ [![Ko-fi](https://img.shields.io/badge/Ko--fi-F16061?style=flat&logo=ko-fi&logoColor=white)](https://ko-fi.com/luciodaou)
22
+ [![PyPI - Version](https://img.shields.io/pypi/v/fm35_decoder.svg)](https://pypi.org/project/fm35_decoder)
23
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fm35_decoder.svg)](https://pypi.org/project/fm35_decoder)
24
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
25
+
26
+ A fast, robust Python tool to decode standard **WMO FM 35 TEMP** upper-air weather messages from radiosonde observations.
27
+
28
+ For an extensive plain-English guide referencing official WMO manuals, see [TEMP_DECODING.md](TEMP_DECODING.md).
29
+
30
+ ---
31
+
32
+ ## Installation
33
+
34
+ Install the package directly from PyPI:
35
+
36
+ ```bash
37
+ pip install fm35_decoder
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Quickstart
43
+
44
+ Import the decoder and call `decode()` with the message parts (`TTAA`, `TTBB`, `TTCC`, `TTDD`):
45
+
46
+ ```python
47
+ from fm35_decoder import decode
48
+
49
+ # Your FM35/TEMP message parts
50
+ ttaa = "TTAA 73121 83779 99938 21224 01008 00163 ///// ///// 92843 20019 07506 85570 18650 36008 ..."
51
+ ttbb = "TTBB 73128 83779 00938 21224 11882 16804 22870 19856 33735 11056 44712 08616 55615 01218 ..."
52
+ ttcc = "TTCC 73123 83779 70865 71568 15020 50064 67574 12519 30380 58383 08535 88906 77162 26018 ..."
53
+ ttdd = "TTDD 7312/ 83779 11906 77162 22585 70370 33542 70970 44445 63978 55283 57785 21212 11935 ..."
54
+
55
+ df_main, df_special = decode(ttaa, ttbb, ttcc, ttdd)
56
+
57
+ # df_main: Clean vertical sounding profile (Pressure, Height, Temp, DewPoint, WindDir, WindSpeed)
58
+ # df_special: Metadata, Tropopause, Maximum Wind, and Cloud groups, with the part each came from
59
+ print(df_main)
60
+ print(df_special)
61
+ ```
62
+
63
+ Any argument may also hold a whole bulletin with several parts: `decode(bulletin)` splits it and
64
+ decodes each part once (`split_bulletin` and `route_parts` do the splitting on their own).
65
+
66
+ ---
67
+
68
+ ## Handling Non-Standard and Extreme Meteorological Data
69
+
70
+ Real-world atmospheric soundings and teletype GTS transmissions frequently contain non-standard encodings, extreme conditions, and transmission artifacts. `fm35_decoder` handles them deterministically per WMO regulations:
71
+
72
+ - **Variable Wind Direction ($dd = 99$)**:
73
+ In WMO Code Table 0877, a wind direction reported as `99` indicates variable wind direction. The decoder sets `WindDir = np.nan` in `df_main` while preserving the numeric `WindSpeed`. Crucially, variable winds are excluded from trigonometric vector ($U/V$) decomposition so they do not distort the vertical wind field.
74
+ - **5-Degree Directional Resolution ($fff \ge 500$)**:
75
+ In WMO FM 35, when the units digit of wind direction is $5^\circ$ (e.g., $275^\circ$), $500$ is added to the reported wind speed ($fff$) across both standard levels and maximum wind groups ($77P_mP_mP_m$). The decoder accurately separates the $5^\circ$ direction component from the true wind speed, avoiding the common $500\text{ kt}$ speed inflation bug.
76
+ - **Calm Wind ($0^\circ, 0\text{ kt}$)**:
77
+ Calm wind is represented as `WindDir = 0.0, WindSpeed = 0.0`. True non-calm winds blowing from North are assigned `360.0°`.
78
+ - **Wind Groups Follow $I_d$**:
79
+ The header's $I_d$ (Code Table 1734) names the last standard level that carries a wind group ($2$ covers $250$ and $200\text{ hPa}$, $1$ covers $150$ and $100\text{ hPa}$; tens of hPa in Part C). Levels above it are read as `PPhhh TTTDD` only, so the next group is never taken for their wind; with $I_d$ = `/` only the surface has a wind group.
80
+ - **Temperatures That Look Like Indicators**:
81
+ $-66.x$ and $-77.x\,^\circ\text{C}$ (common at $150$ and $100\text{ hPa}$ in the tropics) are coded `66xxx` and `77xxx`, like the maximum wind indicators, and `21212` ($21.2\,^\circ\text{C}$) or `31313` ($-31.3\,^\circ\text{C}$) look like section markers. The group after `PPhhh` is read as the temperature by its position; only groups that cannot be a temperature and depression (`88999`, `77999`, `66999`, `41414`, `52525`, `54545`, `55555`, `56565`, `58585`) end a level early.
82
+ - **Maximum Wind Shear**:
83
+ The cloud section (`41414`) belongs to Part B, so in Parts A and C a `41414` after a maximum wind is the shear group ($14\text{ kt}$ below and above).
84
+ - **Wind Speed Units**:
85
+ `YY` is the day plus $50$ when speeds are in knots and the day itself when they are in m/s. m/s speeds are converted to knots in `df_main`; `df_special` keeps the reported value and unit (`240/23m/s`).
86
+ - **Regional and National Sections**:
87
+ `51515`–`59595` and `61616`–`69696` come last in a part; the groups after them are never read as levels (`55555` stays a significant level in Parts B and D, `66666` a maximum wind at $666\text{ hPa}$).
88
+ - **Part B Without Part A**:
89
+ A `TTBB` decodes on its own: the profile has no heights, which only Parts A and C report. A message with fewer than two temperatures (only the surface) raises `ValueError`.
90
+ - **Part C Special Levels in Tenths of hPa**:
91
+ In `TTCC`, `88PtPtPt` and `77PmPmPm` give the pressure in tenths of a hectopascal (`88906` is $90.6\text{ hPa}$).
92
+ - **Pressures Keep Their Tenths**:
93
+ Part D levels are reported in tenths of a hectopascal, and `df_main` keeps them (`70.7` and `70.0 hPa` stay two levels). Round for display in your own code.
94
+ - **Repeated Sections**:
95
+ The `31313` and cloud sections repeat in every part and are listed once. Tropopause and maximum wind entries are all kept: a sounding may report two tropopauses (one in Part A, one in Part C). The `Part` column of `df_special` tells them apart.
96
+ - **GTS Transmission Sequence Breaks**:
97
+ If network or teletype line noise drops an intermediate significant level in `TTBB` or `TTDD` (e.g., group sequence jumps directly from `22` to `44`), the decoder recovers automatically rather than terminating or discarding the remainder of the sounding.
98
+ - **Negative Geopotential Heights ($1000\text{ hPa}$ below sea level)**:
99
+ When a $1000\text{ hPa}$ surface lies below sea level or below ground, WMO encodes it as $500 + |h|$. The decoder detects this offset and outputs true negative geopotential heights (e.g., `00520` $\implies -20\text{ gpm}$).
100
+ - **Stratospheric Standard Levels ($P < 10\text{ hPa}$)**:
101
+ Soundings reaching the middle stratosphere in `TTCC` ($7, 5, 3, 2, 1\text{ hPa}$) are evaluated against extended standard atmosphere heights up to $47,800\text{ gpm}$.
102
+ - **Physical Super-Saturation Prevention**:
103
+ Temperature inversions can cause vertical DewPoint interpolation to cross above Temperature. The decoder strictly enforces $T_d \le T$ ($\text{RH} \le 100\%$).
104
+
105
+ ---
106
+
107
+ ## Meteorological Methodology
108
+
109
+ ### Vertical Interpolation
110
+ - **Temperature & Dewpoint**: Missing intermediate levels are filled via linear interpolation with respect to the **natural logarithm of pressure** ($\ln P$), only between two observed levels: nothing is extrapolated above the last observation or below the first (a top level reported as `/////` stays empty). This aligns with atmospheric thermodynamics on Skew-T / Log-P diagrams where temperature varies approximately linearly with $\ln P$.
111
+ - **Wind Vectors**: Wind speed and direction are decomposed into orthogonal **$U$ (zonal)** and **$V$ (meridional)** vector components. Each component is interpolated independently in $\ln P$ space (again only between observed winds) before being recombined, preventing boundary artifacts across the $0^\circ/360^\circ$ North transition. A variable wind keeps its speed and no direction.
112
+
113
+ ### Hypsometric Geopotential Calculation
114
+ Missing geopotential heights are computed using the Hypsometric Equation:
115
+ $$Z_2 - Z_1 = \frac{R_d \cdot \bar{T}_v}{g_0} \ln\left(\frac{P_1}{P_2}\right)$$
116
+ The decoder incorporates **virtual temperature ($T_v$)** when moisture data is available, accounting for air density variations in warm and humid tropical boundary layers.
117
+
118
+ ---
119
+
120
+ ## Performance & Architecture
121
+
122
+ - **In-Memory WMO Code Tables**: Core WMO tables (3931, 0777, 2700, 0513, 1600, 0515, 0509, 3849, 3872, and Common Code Table C-2) are compiled directly into memory in `tables.py`, providing sub-microsecond table access with zero disk I/O overhead.
123
+ - **NumPy Vectorization**: Hypsometric height integration and vector wind calculations leverage NumPy array operations for speed when batch-processing large archives of historical radiosonde soundings.
124
+
125
+ ---
126
+
127
+ ## Output Data Structure
128
+
129
+ ### `df_main` (Vertical Profile)
130
+ ```
131
+ Pressure Height Temp DewPoint WindDir WindSpeed
132
+ 1000.0 163 20.4 18.0 10.0 8.0
133
+ 938.0 714 21.2 18.8 10.0 8.0
134
+ 925.0 843 20.0 18.1 75.0 6.0
135
+ 882.0 1249 16.8 16.4 23.0 8.5
136
+ 870.0 1366 19.8 13.8 15.0 10.0
137
+ 850.0 1570 18.6 13.6 360.0 8.0
138
+ ... ... ... ... ... ...
139
+ 30.0 23800 -58.3 -91.3 85.0 35.0
140
+ 20.0 26480 -55.2 -89.0 90.0 38.0
141
+ 10.0 31020 -44.1 NaN 95.0 42.0
142
+ ```
143
+
144
+ ### `df_special` (Metadata & Special Levels)
145
+ ```
146
+ Symbol Subject Description Value Part
147
+ h Cloud Base Height 600-1000m (2000-3300ft) TTBB
148
+ Nh Cloud Amount 8 oktas TTBB
149
+ CL Cloud Low Type Stratus nebulosus and/or Stratus fractus TTBB
150
+ CM Cloud Mid Type No CM clouds TTBB
151
+ CH Cloud High Type No CH clouds TTBB
152
+ sr Solar/Inst Solar Corr NOAA solar corrected TTAA
153
+ rara Solar/Inst Sonde Type Vaisala RS41/DigiCORA MW41 (Finland) TTAA
154
+ sasa Solar/Inst Tracking Radar (5 cm) TTAA
155
+ 8GGgg Solar/Inst Time 11:31 TTAA
156
+ PtPtPt Tropopause Pressure 90.6hPa TTCC
157
+ TtTtTt Tropopause Temperature -77.1C TTCC
158
+ DtDt Tropopause Dewpoint -89.1C TTCC
159
+ dtdtftft Tropopause Wind 260/18kt TTCC
160
+ PmPmPm Max Wind Pressure 200hPa TTAA
161
+ dmdmfmfmfm Max Wind Wind 275/120kt TTAA
162
+ vbvb Max Wind Shear Below 14kt TTAA
163
+ vava Max Wind Shear Above 14kt TTAA
164
+ ```
165
+
166
+ ---
167
+
168
+ ## License
169
+
170
+ This project is licensed under the terms of the **MIT License**.
171
+ You are free to use, modify, and distribute this software with attribution to the original author.
@@ -0,0 +1,153 @@
1
+ # FM35 Decoder
2
+
3
+ [![Ko-fi](https://img.shields.io/badge/Ko--fi-F16061?style=flat&logo=ko-fi&logoColor=white)](https://ko-fi.com/luciodaou)
4
+ [![PyPI - Version](https://img.shields.io/pypi/v/fm35_decoder.svg)](https://pypi.org/project/fm35_decoder)
5
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fm35_decoder.svg)](https://pypi.org/project/fm35_decoder)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ A fast, robust Python tool to decode standard **WMO FM 35 TEMP** upper-air weather messages from radiosonde observations.
9
+
10
+ For an extensive plain-English guide referencing official WMO manuals, see [TEMP_DECODING.md](TEMP_DECODING.md).
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ Install the package directly from PyPI:
17
+
18
+ ```bash
19
+ pip install fm35_decoder
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Quickstart
25
+
26
+ Import the decoder and call `decode()` with the message parts (`TTAA`, `TTBB`, `TTCC`, `TTDD`):
27
+
28
+ ```python
29
+ from fm35_decoder import decode
30
+
31
+ # Your FM35/TEMP message parts
32
+ ttaa = "TTAA 73121 83779 99938 21224 01008 00163 ///// ///// 92843 20019 07506 85570 18650 36008 ..."
33
+ ttbb = "TTBB 73128 83779 00938 21224 11882 16804 22870 19856 33735 11056 44712 08616 55615 01218 ..."
34
+ ttcc = "TTCC 73123 83779 70865 71568 15020 50064 67574 12519 30380 58383 08535 88906 77162 26018 ..."
35
+ ttdd = "TTDD 7312/ 83779 11906 77162 22585 70370 33542 70970 44445 63978 55283 57785 21212 11935 ..."
36
+
37
+ df_main, df_special = decode(ttaa, ttbb, ttcc, ttdd)
38
+
39
+ # df_main: Clean vertical sounding profile (Pressure, Height, Temp, DewPoint, WindDir, WindSpeed)
40
+ # df_special: Metadata, Tropopause, Maximum Wind, and Cloud groups, with the part each came from
41
+ print(df_main)
42
+ print(df_special)
43
+ ```
44
+
45
+ Any argument may also hold a whole bulletin with several parts: `decode(bulletin)` splits it and
46
+ decodes each part once (`split_bulletin` and `route_parts` do the splitting on their own).
47
+
48
+ ---
49
+
50
+ ## Handling Non-Standard and Extreme Meteorological Data
51
+
52
+ Real-world atmospheric soundings and teletype GTS transmissions frequently contain non-standard encodings, extreme conditions, and transmission artifacts. `fm35_decoder` handles them deterministically per WMO regulations:
53
+
54
+ - **Variable Wind Direction ($dd = 99$)**:
55
+ In WMO Code Table 0877, a wind direction reported as `99` indicates variable wind direction. The decoder sets `WindDir = np.nan` in `df_main` while preserving the numeric `WindSpeed`. Crucially, variable winds are excluded from trigonometric vector ($U/V$) decomposition so they do not distort the vertical wind field.
56
+ - **5-Degree Directional Resolution ($fff \ge 500$)**:
57
+ In WMO FM 35, when the units digit of wind direction is $5^\circ$ (e.g., $275^\circ$), $500$ is added to the reported wind speed ($fff$) across both standard levels and maximum wind groups ($77P_mP_mP_m$). The decoder accurately separates the $5^\circ$ direction component from the true wind speed, avoiding the common $500\text{ kt}$ speed inflation bug.
58
+ - **Calm Wind ($0^\circ, 0\text{ kt}$)**:
59
+ Calm wind is represented as `WindDir = 0.0, WindSpeed = 0.0`. True non-calm winds blowing from North are assigned `360.0°`.
60
+ - **Wind Groups Follow $I_d$**:
61
+ The header's $I_d$ (Code Table 1734) names the last standard level that carries a wind group ($2$ covers $250$ and $200\text{ hPa}$, $1$ covers $150$ and $100\text{ hPa}$; tens of hPa in Part C). Levels above it are read as `PPhhh TTTDD` only, so the next group is never taken for their wind; with $I_d$ = `/` only the surface has a wind group.
62
+ - **Temperatures That Look Like Indicators**:
63
+ $-66.x$ and $-77.x\,^\circ\text{C}$ (common at $150$ and $100\text{ hPa}$ in the tropics) are coded `66xxx` and `77xxx`, like the maximum wind indicators, and `21212` ($21.2\,^\circ\text{C}$) or `31313` ($-31.3\,^\circ\text{C}$) look like section markers. The group after `PPhhh` is read as the temperature by its position; only groups that cannot be a temperature and depression (`88999`, `77999`, `66999`, `41414`, `52525`, `54545`, `55555`, `56565`, `58585`) end a level early.
64
+ - **Maximum Wind Shear**:
65
+ The cloud section (`41414`) belongs to Part B, so in Parts A and C a `41414` after a maximum wind is the shear group ($14\text{ kt}$ below and above).
66
+ - **Wind Speed Units**:
67
+ `YY` is the day plus $50$ when speeds are in knots and the day itself when they are in m/s. m/s speeds are converted to knots in `df_main`; `df_special` keeps the reported value and unit (`240/23m/s`).
68
+ - **Regional and National Sections**:
69
+ `51515`–`59595` and `61616`–`69696` come last in a part; the groups after them are never read as levels (`55555` stays a significant level in Parts B and D, `66666` a maximum wind at $666\text{ hPa}$).
70
+ - **Part B Without Part A**:
71
+ A `TTBB` decodes on its own: the profile has no heights, which only Parts A and C report. A message with fewer than two temperatures (only the surface) raises `ValueError`.
72
+ - **Part C Special Levels in Tenths of hPa**:
73
+ In `TTCC`, `88PtPtPt` and `77PmPmPm` give the pressure in tenths of a hectopascal (`88906` is $90.6\text{ hPa}$).
74
+ - **Pressures Keep Their Tenths**:
75
+ Part D levels are reported in tenths of a hectopascal, and `df_main` keeps them (`70.7` and `70.0 hPa` stay two levels). Round for display in your own code.
76
+ - **Repeated Sections**:
77
+ The `31313` and cloud sections repeat in every part and are listed once. Tropopause and maximum wind entries are all kept: a sounding may report two tropopauses (one in Part A, one in Part C). The `Part` column of `df_special` tells them apart.
78
+ - **GTS Transmission Sequence Breaks**:
79
+ If network or teletype line noise drops an intermediate significant level in `TTBB` or `TTDD` (e.g., group sequence jumps directly from `22` to `44`), the decoder recovers automatically rather than terminating or discarding the remainder of the sounding.
80
+ - **Negative Geopotential Heights ($1000\text{ hPa}$ below sea level)**:
81
+ When a $1000\text{ hPa}$ surface lies below sea level or below ground, WMO encodes it as $500 + |h|$. The decoder detects this offset and outputs true negative geopotential heights (e.g., `00520` $\implies -20\text{ gpm}$).
82
+ - **Stratospheric Standard Levels ($P < 10\text{ hPa}$)**:
83
+ Soundings reaching the middle stratosphere in `TTCC` ($7, 5, 3, 2, 1\text{ hPa}$) are evaluated against extended standard atmosphere heights up to $47,800\text{ gpm}$.
84
+ - **Physical Super-Saturation Prevention**:
85
+ Temperature inversions can cause vertical DewPoint interpolation to cross above Temperature. The decoder strictly enforces $T_d \le T$ ($\text{RH} \le 100\%$).
86
+
87
+ ---
88
+
89
+ ## Meteorological Methodology
90
+
91
+ ### Vertical Interpolation
92
+ - **Temperature & Dewpoint**: Missing intermediate levels are filled via linear interpolation with respect to the **natural logarithm of pressure** ($\ln P$), only between two observed levels: nothing is extrapolated above the last observation or below the first (a top level reported as `/////` stays empty). This aligns with atmospheric thermodynamics on Skew-T / Log-P diagrams where temperature varies approximately linearly with $\ln P$.
93
+ - **Wind Vectors**: Wind speed and direction are decomposed into orthogonal **$U$ (zonal)** and **$V$ (meridional)** vector components. Each component is interpolated independently in $\ln P$ space (again only between observed winds) before being recombined, preventing boundary artifacts across the $0^\circ/360^\circ$ North transition. A variable wind keeps its speed and no direction.
94
+
95
+ ### Hypsometric Geopotential Calculation
96
+ Missing geopotential heights are computed using the Hypsometric Equation:
97
+ $$Z_2 - Z_1 = \frac{R_d \cdot \bar{T}_v}{g_0} \ln\left(\frac{P_1}{P_2}\right)$$
98
+ The decoder incorporates **virtual temperature ($T_v$)** when moisture data is available, accounting for air density variations in warm and humid tropical boundary layers.
99
+
100
+ ---
101
+
102
+ ## Performance & Architecture
103
+
104
+ - **In-Memory WMO Code Tables**: Core WMO tables (3931, 0777, 2700, 0513, 1600, 0515, 0509, 3849, 3872, and Common Code Table C-2) are compiled directly into memory in `tables.py`, providing sub-microsecond table access with zero disk I/O overhead.
105
+ - **NumPy Vectorization**: Hypsometric height integration and vector wind calculations leverage NumPy array operations for speed when batch-processing large archives of historical radiosonde soundings.
106
+
107
+ ---
108
+
109
+ ## Output Data Structure
110
+
111
+ ### `df_main` (Vertical Profile)
112
+ ```
113
+ Pressure Height Temp DewPoint WindDir WindSpeed
114
+ 1000.0 163 20.4 18.0 10.0 8.0
115
+ 938.0 714 21.2 18.8 10.0 8.0
116
+ 925.0 843 20.0 18.1 75.0 6.0
117
+ 882.0 1249 16.8 16.4 23.0 8.5
118
+ 870.0 1366 19.8 13.8 15.0 10.0
119
+ 850.0 1570 18.6 13.6 360.0 8.0
120
+ ... ... ... ... ... ...
121
+ 30.0 23800 -58.3 -91.3 85.0 35.0
122
+ 20.0 26480 -55.2 -89.0 90.0 38.0
123
+ 10.0 31020 -44.1 NaN 95.0 42.0
124
+ ```
125
+
126
+ ### `df_special` (Metadata & Special Levels)
127
+ ```
128
+ Symbol Subject Description Value Part
129
+ h Cloud Base Height 600-1000m (2000-3300ft) TTBB
130
+ Nh Cloud Amount 8 oktas TTBB
131
+ CL Cloud Low Type Stratus nebulosus and/or Stratus fractus TTBB
132
+ CM Cloud Mid Type No CM clouds TTBB
133
+ CH Cloud High Type No CH clouds TTBB
134
+ sr Solar/Inst Solar Corr NOAA solar corrected TTAA
135
+ rara Solar/Inst Sonde Type Vaisala RS41/DigiCORA MW41 (Finland) TTAA
136
+ sasa Solar/Inst Tracking Radar (5 cm) TTAA
137
+ 8GGgg Solar/Inst Time 11:31 TTAA
138
+ PtPtPt Tropopause Pressure 90.6hPa TTCC
139
+ TtTtTt Tropopause Temperature -77.1C TTCC
140
+ DtDt Tropopause Dewpoint -89.1C TTCC
141
+ dtdtftft Tropopause Wind 260/18kt TTCC
142
+ PmPmPm Max Wind Pressure 200hPa TTAA
143
+ dmdmfmfmfm Max Wind Wind 275/120kt TTAA
144
+ vbvb Max Wind Shear Below 14kt TTAA
145
+ vava Max Wind Shear Above 14kt TTAA
146
+ ```
147
+
148
+ ---
149
+
150
+ ## License
151
+
152
+ This project is licensed under the terms of the **MIT License**.
153
+ You are free to use, modify, and distribute this software with attribution to the original author.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fm35_decoder"
3
- version = "0.1.2"
3
+ version = "1.1"
4
4
  description = "A Python tool to decode standard FM35 TEMP weather messages from upper-air observations."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -0,0 +1,3 @@
1
+ from .decoder import decode, decode_full, route_parts, split_bulletin
2
+
3
+ __all__ = ["decode", "decode_full", "route_parts", "split_bulletin"]