From dc4e6f3423ebd01df86a7188988ef9cc23b31348 Mon Sep 17 00:00:00 2001
From: XANTRONIX Industrial <xan@xantronix.com>
Date: Mon, 24 Feb 2025 17:25:18 -0500
Subject: [PATCH] Initial commit of sounding.py

---
 lib/xmet/sounding.py | 66 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 66 insertions(+)
 create mode 100644 lib/xmet/sounding.py

diff --git a/lib/xmet/sounding.py b/lib/xmet/sounding.py
new file mode 100644
index 0000000..62b8529
--- /dev/null
+++ b/lib/xmet/sounding.py
@@ -0,0 +1,66 @@
+import datetime
+import shapely
+
+LAPSE_RATE_DRY   = 9.8 / 1000 # degrees C per 1000m
+LAPSE_RATE_MOIST = 4.0 / 1000
+
+MIXING_RATIO_CONST = 621.97
+
+class SoundingSample():
+    __slots__ = (
+        'id', 'sounding_id', 'elapsed', 'pressure', 'pressure_qa',
+        'height', 'height_qa', 'temp', 'temp_qa', 'humidity',
+        'dewpoint', 'wind_dir', 'wind_speed'
+    )
+
+    id:          int
+    sounding_id: int
+    elapsed:     int
+    pressure:    float
+    pressure_qa: str
+    height:      float
+    height_qa:   str
+    temp:        float
+    temp_qa:     str
+    humidity:    float
+    dewpoint:    float
+    wind_dir:    float
+    wind_speed:  float
+
+    def vapor_pressure(self) -> float:
+        return 6.11 * 10 * (
+            (7.5 * self.dewpoint) / (237.3 * self.dewpoint)
+        )
+
+    def saturated_vapor_pressure(self) -> float:
+        return 6.11 * 10 * (
+            (7.5 * self.temp) / (237.3 * self.temp)
+        )
+
+    def mixing_ratio(self) -> float:
+        e = self.vapor_pressure()
+        
+        return 621.97 * (e / (self.pressure - e))
+
+    def saturated_mixing_ratio(self) -> float:
+        es = self.saturated_vapor_pressure()
+
+        return 621.97 * (es / (self.pressure - es))
+
+    def is_saturated(self) -> bool:
+        return self.humidity >= 100.0
+
+class Sounding():
+    __slots__ = (
+        'id', 'station', 'timestamp_observed', 'timestamp_released',
+        'data_source_pressure', 'data_source_other', 'samples', 'coord'
+    )
+
+    id:                   int
+    station:              str
+    timestamp_observed:   datetime.datetime
+    timestamp_released:   datetime.datetime
+    data_source_pressure: str
+    data_source_other:    str
+    coord:                shapely.Point
+    samples:              list[SoundingSample]