From 53ea99803e8ded8b066a036595217d94509e674f Mon Sep 17 00:00:00 2001
From: XANTRONIX Industrial <xan@xantronix.com>
Date: Sat, 8 Mar 2025 12:02:42 -0500
Subject: [PATCH] Move parameter derivation to sounding.py

---
 lib/xmet/skew_t.py   | 86 --------------------------------------------
 lib/xmet/sounding.py | 49 +++++++++++++++++++++++--
 2 files changed, 47 insertions(+), 88 deletions(-)

diff --git a/lib/xmet/skew_t.py b/lib/xmet/skew_t.py
index 92807c0..9045e4e 100644
--- a/lib/xmet/skew_t.py
+++ b/lib/xmet/skew_t.py
@@ -180,92 +180,6 @@ class SkewTGraph():
         cr.stroke()
         cr.restore()
 
-    def follow_dry_adiabat(self, temp: float, pressure: float) -> dict:
-        levels = dict()
-
-        for level in loft_parcel(temp, pressure, lambda t, p: LAPSE_RATE_DRY):
-            t2, p2 = level
-
-            levels[p2] = t2
-
-        return levels
-
-    def follow_moist_adiabat(self, temp: float, pressure: float) -> dict:
-        levels = dict()
-
-        for level in loft_parcel(temp, pressure, moist_lapse_rate):
-            t2, p2 = level
-
-            levels[p2] = t2
-
-        return levels
-
-    def follow_saturated_mixing_ratio(self, temp: float, pressure: float) -> dict:
-        levels = dict()
-
-        ratio = saturated_mixing_ratio(temp, pressure)
-
-        p2 = pressure
-
-        while p2 >= PRESSURE_MIN:
-            levels[p2] = mixing_ratio_temp(ratio, p2)
-
-            p2 -= 10
-
-        return levels
-
-    def follow_sounding(self, sounding: Sounding) -> dict:
-        levels = dict()
-
-        for sample in sounding.samples:
-            levels[sample.pressure] = sample.temp
-
-        return levels
-
-    def intersect(self, series1: dict, series2: dict):
-        pairs = nearest(sorted(series1.keys(), reverse=True),
-                        sorted(series2.keys(), reverse=True))
-
-        sign_last = None
-
-        for pair in pairs:
-            v1, v2 = series1[pair[0]], series2[pair[1]]
-
-            sign = cmp(v1, v2)
-
-            if sign == 0 or (sign_last is not None and sign_last != sign):
-                return pair[0]
-
-            sign_last = sign
-
-    def find_lfc(self, temp: float, pressure: float, sounding: Sounding) -> float:
-        moist_adiabat = self.follow_moist_adiabat(temp, pressure)
-        temp_line     = self.follow_sounding(sounding)
-
-        pairs = nearest(sorted(moist_adiabat.keys(), reverse=True),
-                        sorted(temp_line.keys(),     reverse=True))
-
-        for pair in pairs:
-            v1, v2 = moist_adiabat[pair[0]], temp_line[pair[1]]
-
-            if v1 > v2:
-                return pair[0]
-
-    def derive_parameters(self, sounding: Sounding):
-        dry_adiabat = self.follow_dry_adiabat(sounding.samples[0].temp,
-                                              sounding.samples[0].pressure)
-
-        saturated_mr_line = self.follow_saturated_mixing_ratio(sounding.samples[0].dewpoint,
-                                                               sounding.samples[0].pressure)
-
-        lcl = self.intersect(dry_adiabat, saturated_mr_line)
-        lfc = self.find_lfc(dry_adiabat[lcl], lcl, sounding)
-
-        return {
-            'lcl': lcl,
-            'lfc': lfc
-        }
-
     def draw(self,
              cr: cairo.Context,
              x: float,
diff --git a/lib/xmet/sounding.py b/lib/xmet/sounding.py
index fc120db..44fdb7f 100644
--- a/lib/xmet/sounding.py
+++ b/lib/xmet/sounding.py
@@ -1,8 +1,13 @@
 import datetime
 import shapely
 
-from xmet.db    import Database, DatabaseTable, DatabaseOrder
-from xmet.coord import COORD_SYSTEM
+from xmet.db     import Database, DatabaseTable, DatabaseOrder
+from xmet.coord  import COORD_SYSTEM
+from xmet.list   import nearest
+from xmet.series import Series
+from xmet.thermo import follow_dry_adiabat, \
+                        follow_moist_adiabat, \
+                        follow_saturated_mixing_ratio
 
 LAPSE_RATE_DRY   = 9.8 # degrees C per 1000m
 LAPSE_RATE_MOIST = 4.0
@@ -42,6 +47,9 @@ class SoundingSample(DatabaseTable):
     def is_saturated(self) -> bool:
         return self.humidity >= 100.0
 
+class SoundingParameters():
+    pass
+
 class Sounding(DatabaseTable):
     __slots__ = (
         'id', 'station', 'timestamp_observed', 'timestamp_released',
@@ -168,3 +176,40 @@ class Sounding(DatabaseTable):
             ]]).fetchall())
 
         return sounding
+
+    def follow_temp(self) -> Series:
+        series = Series()
+
+        for sample in self.samples:
+            series[sample.pressure] = sample.temp
+
+        return series
+
+    def find_lfc(self, temp: float, pressure: float) -> float:
+        moist_adiabat = follow_moist_adiabat(temp, pressure)
+        temp_line     = self.follow_temp()
+
+        pairs = nearest(sorted(moist_adiabat.keys(), reverse=True),
+                        sorted(temp_line.keys(),     reverse=True))
+
+        for pair in pairs:
+            v1, v2 = moist_adiabat[pair[0]], temp_line[pair[1]]
+
+            if v1 > v2:
+                return pair[0]
+
+    def derive_parameters(self) -> SoundingParameters:
+        dry_adiabat = follow_dry_adiabat(self.samples[0].temp,
+                                         self.samples[0].pressure)
+
+        saturated_mr_line = follow_saturated_mixing_ratio(self.samples[0].dewpoint,
+                                                          self.samples[0].pressure)
+
+        lcl = dry_adiabat.intersect(saturated_mr_line)
+        lfc = self.find_lfc(dry_adiabat[lcl], lcl)
+
+        params = SoundingParameters()
+        params.lcl = lcl
+        params.lfc = lfc
+
+        return params