Skip to content

Commit 8edff11

Browse files
committed
ADD: add possibility to retrieve soundings over MCH DWH
1 parent bd12636 commit 8edff11

File tree

6 files changed

+225
-71
lines changed

6 files changed

+225
-71
lines changed

src/pyrad_proc/pyrad/io/__init__.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@
111111
read_ml_ts
112112
read_windmills_data
113113
read_radiosounding_wyoming
114-
read_radiosounding_igra
115-
read_fzl_igra
114+
retrieve_radiosounding_igra
115+
retrieve_fzl
116116
read_meteoswiss_xml_vad
117117
118118
Writing data
@@ -255,10 +255,11 @@
255255
from .read_data_sensor import read_coord_sensors, read_disdro_parsivel # noqa
256256
from .read_data_sensor import read_knmi # noqa
257257
from .read_data_sensor import ( # noqa
258-
read_radiosounding_wyoming,
259-
read_radiosounding_igra,
260-
)
261-
from .read_data_sensor import read_fzl_igra # noqa
258+
retrieve_radiosounding_wyoming, # noqa
259+
retrieve_radiosounding_igra, # noqa
260+
retrieve_radiosounding_mch, # noqa
261+
) # noqa
262+
from .read_data_sensor import retrieve_fzl # noqa
262263

263264
from .read_data_sun import read_sun_hits_multiple_days, read_sun_hits # noqa
264265
from .read_data_sun import read_sun_retrieval, read_solar_flux # noqa

src/pyrad_proc/pyrad/io/read_data_sensor.py

Lines changed: 163 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
read_disdro
3232
read_disdro_parsivel
3333
read_radiosounding_wyoming
34-
read_radiosounding_igra
35-
read_fzl_igra
34+
retrieve_radiosounding_igra
35+
retrieve_fzl
3636
"""
3737

3838
import os
@@ -58,6 +58,22 @@
5858

5959
urllib3.disable_warnings(InsecureRequestWarning)
6060

61+
COLUMNS_SOUNDING = [
62+
"LVLTYPE1",
63+
"LVLTYPE2",
64+
"ETIME",
65+
"PRESS",
66+
"PFLAG",
67+
"GPH",
68+
"ZFLAG",
69+
"TEMP",
70+
"TFLAG",
71+
"RH",
72+
"DPDP",
73+
"WDIR",
74+
"WSPD",
75+
]
76+
6177

6278
def read_windmills_data(fname):
6379
"""
@@ -2517,22 +2533,24 @@ def read_disdro_parsivel(fname):
25172533
return df_disdro
25182534

25192535

2520-
def read_radiosounding_wyoming(station, dtime):
2536+
def retrieve_radiosounding_wyoming(station, dtime):
25212537
"""
2522-
Download radiosounding data from the University of Wyoming website.
2538+
Download radiosounding data from the University of Wyoming website and
2539+
return as DataFrame with IGRA2-standard columns.
25232540
2524-
Parameters:
2525-
station : str
2526-
Radiosounding station 5 digits WMO code (e.g., "72776").
2527-
dtime : datetime.datetime
2528-
Datetime object specifying the desired date and time.
2541+
Parameters
2542+
----------
2543+
station : str
2544+
Radiosounding station 5-digit WMO code (e.g., "72776").
2545+
dtime : datetime.datetime
2546+
Datetime object specifying the desired date and time.
25292547
2530-
Returns:
2531-
pandas.DataFrame: A DataFrame containing the radiosounding data with
2532-
columns: ['PRES', 'HGHT', 'TEMP', 'DWPT', 'RELH', 'MIXR', 'DRCT',
2533-
'SKNT', 'THTA', 'THTE', 'THTV]
2548+
Returns
2549+
-------
2550+
pandas.DataFrame or None
2551+
A DataFrame containing the radiosounding data in IGRA2 format,
2552+
or None if retrieval was not successful.
25342553
"""
2535-
25362554
base_url = "https://weather.uwyo.edu/cgi-bin/sounding"
25372555
query_params = {
25382556
"region": "naconf",
@@ -2545,36 +2563,68 @@ def read_radiosounding_wyoming(station, dtime):
25452563
}
25462564

25472565
try:
2548-
# Try a quick HEAD request to test SSL verification
2566+
# Quick HEAD request to handle SSL verification issues
25492567
requests.head(base_url, timeout=5)
25502568
verify_setting = True
25512569
except requests.exceptions.SSLError:
25522570
verify_setting = False
25532571

25542572
response = requests.get(base_url, params=query_params, verify=verify_setting)
2555-
25562573
if response.status_code != 200:
2574+
warn(f"Failed to retrieve data: HTTP {response.status_code}")
25572575
return None
25582576

2559-
# Extract and parse the data using pandas
2577+
# Extract text block
25602578
start_idx = response.text.find("PRES")
25612579
end_idx = response.text.find("Station information and sounding indices")
2562-
data_str = response.text[start_idx:end_idx][0:-10]
2580+
if start_idx == -1 or end_idx == -1:
2581+
warn("Could not locate sounding data block in response")
2582+
return None
25632583

2584+
data_str = response.text[start_idx:end_idx].strip()
25642585
data_io = StringIO(data_str)
2565-
data_df = pd.read_csv(
2566-
data_io,
2567-
sep=r"\s+",
2568-
header=0,
2569-
skiprows=[1, 2],
2570-
on_bad_lines="warn",
2571-
)
2586+
2587+
# Parse table (skip line with units)
2588+
try:
2589+
data_df = pd.read_csv(
2590+
data_io,
2591+
sep=r"\s+",
2592+
header=0,
2593+
skiprows=[1],
2594+
on_bad_lines="skip",
2595+
)
2596+
except Exception as e:
2597+
warn(f"Parsing error: {e}")
2598+
return None
2599+
2600+
# Convert to numeric where possible
25722601
for col in data_df.columns:
2573-
data_df[col] = pd.to_numeric(data_df[col])
2574-
return data_df
2602+
data_df[col] = pd.to_numeric(data_df[col], errors="coerce")
2603+
2604+
# Map Wyoming columns to IGRA2 columns
2605+
df = pd.DataFrame(
2606+
{
2607+
"LVLTYPE1": "", # Not available from Wyoming
2608+
"LVLTYPE2": "",
2609+
"ETIME": dtime.strftime("%Y%m%d%H"), # assign launch time to all rows
2610+
"PRESS": data_df.get("PRES"),
2611+
"PFLAG": "",
2612+
"GPH": data_df.get("HGHT"),
2613+
"ZFLAG": "",
2614+
"TEMP": data_df.get("TEMP"),
2615+
"TFLAG": "",
2616+
"RH": data_df.get("RELH"),
2617+
"DPDP": data_df.get("DWPT"),
2618+
"WDIR": data_df.get("DRCT"),
2619+
"WSPD": data_df.get("SKNT") * 0.514444, # knots → m/s
2620+
},
2621+
columns=COLUMNS_SOUNDING,
2622+
)
25752623

2624+
return df
25762625

2577-
def read_radiosounding_igra(station, dtime):
2626+
2627+
def retrieve_radiosounding_igra(station, dtime):
25782628
"""
25792629
Download radiosounding data from from the Integrated Global Radiosonde Archive
25802630
(IGRA).
@@ -2594,22 +2644,6 @@ def read_radiosounding_igra(station, dtime):
25942644
https://www.ncei.noaa.gov/data/integrated-global-radiosonde-archive/doc/igra2-data-format.txt
25952645
"""
25962646
print("Downloading sounding from IGRA database...")
2597-
2598-
COLUMNS_SOUNDING = [
2599-
"LVLTYPE1",
2600-
"LVLTYPE2",
2601-
"ETIME",
2602-
"PRESS",
2603-
"PFLAG",
2604-
"GPH",
2605-
"ZFLAG",
2606-
"TEMP",
2607-
"TFLAG",
2608-
"RH",
2609-
"DPDP",
2610-
"WDIR",
2611-
"WSPD",
2612-
]
26132647
COLUMNS_STATION_LIST = [
26142648
"CODE",
26152649
"LATITUDE",
@@ -2752,10 +2786,82 @@ def read_radiosounding_igra(station, dtime):
27522786
return sounding
27532787

27542788

2755-
def read_fzl_igra(station, dtime, dscfg=None):
2789+
def retrieve_radiosounding_mch(station, dtime):
27562790
"""
2757-
Get an estimation of the freezing level height from the
2758-
Integrated Global Radiosonde Archive (IGRA)
2791+
Retrieve sounding data from MCH DWH service and return as pandas DataFrame.
2792+
It requires to set the environment variable $JRETRIEVE_PATH that points to a
2793+
valid jretrieve executable.
2794+
2795+
Parameters:
2796+
station : str
2797+
Radiosounding station 5 digits WMO code (e.g., "72776").
2798+
dtime : datetime.datetime
2799+
Datetime object specifying the desired date and time.
2800+
2801+
Returns:
2802+
pandas.DataFrame: A DataFrame containing the radiosounding data with
2803+
['LVLTYPE1', 'LVLTYPE2','ETIME', 'PRESS', 'PFLAG', 'GPH','ZFLAG', 'TEMP',
2804+
'TFLAG', 'RH','DPDP','WDIR','WSPD']
2805+
2806+
"""
2807+
print("Downloading sounding from MCH DWH database...")
2808+
2809+
dtime_closest = _closest_sounding_time(dtime)
2810+
dtime_closest = dtime_closest.strftime("%Y%m%d%H%M%S")
2811+
2812+
# Variables to retrieve (DWH IDs mapped to IGRA2 columns)
2813+
id_variables = "744,742,745,747,748,743" # p, geopotential, T, Td, ws, wd
2814+
2815+
# Build DWH command
2816+
ret_dwh = (
2817+
f'{os.path.expandvars("$JRETRIEVE_PATH")} -s profile -w 22 --verbose position '
2818+
f"-i int_ind,{station} "
2819+
f"-t {dtime_closest} -p {id_variables}"
2820+
)
2821+
try:
2822+
with os.popen(ret_dwh) as pipefile:
2823+
lines = pipefile.readlines()
2824+
2825+
rows = []
2826+
for line in lines:
2827+
if dtime_closest in line:
2828+
vals = line.split("|")
2829+
# Extract relevant fields by index (adjust if needed)
2830+
rows.append(
2831+
{
2832+
"LVLTYPE1": vals[4].strip(),
2833+
"LVLTYPE2": vals[3].strip(),
2834+
"ETIME": vals[1].strip(),
2835+
"PRESS": float(vals[8].strip()),
2836+
"PFLAG": "", # no flag info available from DWH
2837+
"GPH": float(vals[9].strip()),
2838+
"ZFLAG": "",
2839+
"TEMP": float(vals[10].strip()),
2840+
"TFLAG": "",
2841+
"RH": "", # not retrieved
2842+
"DPDP": float(vals[11].strip()),
2843+
"WDIR": float(vals[13].strip()),
2844+
"WSPD": float(vals[12].strip()),
2845+
}
2846+
)
2847+
2848+
if not rows:
2849+
warn(f"No valid data returned by DWH call. Return: {lines}")
2850+
return None
2851+
2852+
df = pd.DataFrame(rows, columns=COLUMNS_SOUNDING)
2853+
return df
2854+
2855+
except EnvironmentError:
2856+
warn(f"Unable to read DWH data with call {ret_dwh}")
2857+
return None
2858+
2859+
2860+
def retrieve_fzl(station, dtime, dscfg=None, source="IGRA"):
2861+
"""
2862+
Get an estimation of the freezing level height either the
2863+
Integrated Global Radiosonde Archive (IGRA), University of Wyoming archive
2864+
or the MCH digital warehouse (DWH)
27592865
27602866
Parameters:
27612867
station : str
@@ -2765,10 +2871,11 @@ def read_fzl_igra(station, dtime, dscfg=None):
27652871
dscfg : dictionary of dictionaries, Optional
27662872
If provided will try to read the fzl from the pyrad config
27672873
dictionary instead of fetching it from IGRA
2874+
source: str
2875+
either 'IGRA', "WYOMING" or 'MCH'
27682876
Returns:
27692877
fzl : float
2770-
Interpolated freezing level height (a.s.l) obtained
2771-
from the nearest in time IGRA sounding
2878+
Interpolated freezing level height (a.s.l)
27722879
"""
27732880
if dscfg is None:
27742881
dscfg = {}
@@ -2780,7 +2887,13 @@ def read_fzl_igra(station, dtime, dscfg=None):
27802887
except (KeyError, TypeError):
27812888
pass
27822889

2783-
sounding = read_radiosounding_igra(station, dtime)
2890+
if source == "IGRA":
2891+
sounding = retrieve_radiosounding_igra(station, dtime)
2892+
elif source == "MCH":
2893+
sounding = retrieve_radiosounding_mch(station, dtime)
2894+
elif source == "WYOMING":
2895+
sounding = retrieve_radiosounding_wyoming(station, dtime)
2896+
27842897
height = sounding["GPH"]
27852898
temp = sounding["TEMP"]
27862899

src/pyrad_proc/pyrad/proc/process_Doppler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
get_fieldname_pyart,
4747
convert_pydda_to_pyart_grid,
4848
)
49-
from ..io import read_radiosounding_igra
49+
from ..io import retrieve_radiosounding_igra
5050
from .process_grid import process_grid
5151
from ..util import compute_average_vad
5252

@@ -1067,7 +1067,7 @@ def process_dda(procstatus, dscfg, radar_list=None):
10671067
sounding = dscfg.get("sounding", None)
10681068
if sounding:
10691069
dtime = pyart.graph.common.generate_radar_time_begin(radar_list[0])
1070-
sounding_data = read_radiosounding_igra(sounding, dtime)
1070+
sounding_data = retrieve_radiosounding_igra(sounding, dtime)
10711071
else:
10721072
sounding_data = None
10731073

src/pyrad_proc/pyrad/proc/process_echoclass.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from ..io.io_aux import get_datatype_fields, get_fieldname_pyart
4444
from ..io.io_aux import get_file_list, get_datetime
4545
from ..io.read_data_other import read_centroids
46-
from ..io.read_data_sensor import read_fzl_igra
46+
from ..io.read_data_sensor import retrieve_fzl
4747

4848
if importlib.util.find_spec("sklearn_extra") and importlib.util.find_spec("sklearn"):
4949
_SKLEARN_AVAILABLE = True
@@ -1500,6 +1500,9 @@ def process_hydroclass(procstatus, dscfg, radar_list=None):
15001500
compute the freezing level, if no temperature field name is specified,
15011501
if the temperature field is not in the radar object or if no
15021502
fzl is explicitely defined.
1503+
sounding_source: str. Dataset keyword
1504+
Web source from which to get the sounding data, either "IGRA", "WYOMING", or
1505+
"MCH". If not provided, IGRA is used.
15031506
use_dualpol: Bool. Dataset keyword
15041507
Used with HYDRO_METHOD UKMO. If false no radar data is used and
15051508
the classification is performed using temperature information
@@ -1585,8 +1588,11 @@ def process_hydroclass(procstatus, dscfg, radar_list=None):
15851588
use_debug=False,
15861589
)
15871590
sounding_code = dscfg["sounding"]
1591+
sounding_source = dscfg.get("sounding_source", "IGRA")
15881592
t0 = pyart.util.datetime_utils.datetime_from_radar(radar)
1589-
freezing_level = read_fzl_igra(sounding_code, t0, dscfg=dscfg)
1593+
freezing_level = retrieve_fzl(
1594+
sounding_code, t0, dscfg=dscfg, source=sounding_source
1595+
)
15901596
else:
15911597
warn(
15921598
"iso0 or temperature fields or sounding needed to create hydrometeor "
@@ -1747,8 +1753,11 @@ def process_hydroclass(procstatus, dscfg, radar_list=None):
17471753
freezing_level = dscfg["freezing_level"]
17481754
elif "sounding" in dscfg:
17491755
sounding_code = dscfg["sounding"]
1756+
sounding_source = dscfg.get("sounding_source", "IGRA")
17501757
t0 = pyart.util.datetime_utils.datetime_from_radar(radar)
1751-
freezing_level = read_fzl_igra(sounding_code, t0, dscfg=dscfg)
1758+
freezing_level = retrieve_fzl(
1759+
sounding_code, t0, dscfg=dscfg, source=sounding_source
1760+
)
17521761

17531762
use_dualpol = dscfg.get("use_dualpol", True)
17541763
use_temperature = dscfg.get("use_temperature", True)

0 commit comments

Comments
 (0)