Re: [mcidas-x] Question about receiving GLM data from an ADDE server

  • To: "Sebenste, Gilbert" <sebensteg@xxxxxxx>
  • Subject: Re: [mcidas-x] Question about receiving GLM data from an ADDE server
  • From: Mike Zuranski <zuranski.wx@xxxxxxxxx>
  • Date: Mon, 27 Apr 2026 11:26:31 -0400
Hi Gilbert,

>   NWS says the data is still going out fine.

I haven't checked in a little bit but this has been an on/off problem for
something like a year now.  The one minute aggregations, level2, that data
is still going out last I saw.  But they stopped sending the five minute
aggregations, level3, which is what your site uses.  The argument was end
sites (i.e. WFOs) can make their own five-minute aggregations from the
one-minute data.  This is something AWIPS can do, and since that is the
intended audience... well this is all working as intended then right??

I'm including a python script that can do this aggregation, you'll need to
make a couple edits for your environment.  IIRC I put this on your system
and was about to turn it on when I saw they reinstated flowing level3 data,
so I left it there disabled in case this happened again.  I forget exactly
what state that's in, but you might just need to enable a pqact if you're
lucky.  Look around to see if I already put this script someplace there for
you.

best,
-Mike

On Mon, Apr 27, 2026 at 10:38 AM Sebenste, Gilbert <sebensteg@xxxxxxx>
wrote:

> Good morning everyone,
>
> About a week ago, I suddenly lost our GLM derived flash data for no
> apparent reason. Single strikes are coming in and plotting fine, but
> derived data such as Total Optical Energy (TOE), etc suddenly disappeared
> on my end. NWS says the data is still going out fine.
>
>
> Until I can figure this out on my end, I decided that maybe I should just
> get it from the UNIDATA ADDE server
> for now. I tried to do this:
>
> dsserve.k ADD RTGOESR/GLMEVENT GLMN TYPE=POINT
> INFO=/home/mcidas/data/GLM_EVENT.cfg ADDE.UNIDATA.UCAR.EDU
>
> But it gives me this error:
> dsserve.k: You must enter a min and max file  pair with this dataset.
>
> dsserve.k: done
>
> Can anyone tell me what am I doing wrong?
>
> Thank you!
>
>
>
> Gilbert Sebenste
>
> Meteorology Support Analyst
>
>
> _________________________________________________________
> NOTE: All exchanges posted to NSF Unidata maintained email lists are
> made publicly available through the web. Users who post to any of the
> lists we maintain are reminded to remove any personal information that
> they do not want to be made public.
>
> NSF Unidata mcidas-x Mailing List
> (mcidas-x@xxxxxxxxxxxxxxxx)
> For list information, to unsubscribe, or change your membership options,
> visit: https://mailinglists.unidata.ucar.edu/listinfo/mcidas-x/
>


-- 
- Mike Zuranski

PNG image

#!/home/ldm/.conda/envs/dev/bin/python
"""
Minimal GLM L2→L3 aggregation (5 min L3 from 5 x 1-min L2).

Mike Zuranski - 2026
"""

import pathlib
import re
from datetime import datetime, timedelta
import xarray as xr
import numpy as np
from datetime import datetime as dt

L2_DIR = pathlib.Path("/home/localdata/satellite/glm/GLMISatSS/level2")
L3_DIR = pathlib.Path("/home/localdata/satellite/glm/GLMISatSS/level3")
L3_DIR.mkdir(parents=True, exist_ok=True)


def extract_time(path):
    """Extract start time from OR_GLM-L2 filename."""
    m = re.search(r"s(\d{13})", path.stem)
    if not m:
        raise ValueError(f"Cannot parse time from {path.name}")
    return datetime.strptime(m.group(1), "%Y%j%H%M%S")


def main():
    files = sorted(L2_DIR.glob("OR_GLM-L2-GLMF*.nc"))
    if not files:
        print("No L2 files found")
        return

    times = [extract_time(f) for f in files]

    # Process most recent complete 5-file window
    for i in range(len(times) - 1, -1, -1):
        t_end = times[i]
        t_start = t_end - timedelta(minutes=4)
        window = [f for f, t in zip(files, times) if t_start <= t <= t_end]
        if len(window) == 5:
            aggregate_and_write(window)
            break


def aggregate_and_write(window):
    """Aggregate 5 L2 files into 1 L3 file."""
    print(f"Aggregating {len(window)} files...")

    # Load first file as template
    ds_template = xr.open_dataset(window[0], decode_cf=False)

    # Initialize accumulators
    fed_sum = None
    toe_sum = None
    mfa_min = None

    # Aggregate
    for p in window:
        ds = xr.open_dataset(p, decode_cf=False)

        # FED: sum (counts up)
        if fed_sum is None:
            fed_sum = ds["Flash_extent_density"].values.astype(np.uint32)
        else:
            fed_sum += ds["Flash_extent_density"].values.astype(np.uint32)

        # TOE: sum (cumulative energy)
        if toe_sum is None:
            toe_sum = ds["Total_Optical_energy"].values.astype(np.uint32)
        else:
            toe_sum += ds["Total_Optical_energy"].values.astype(np.uint32)

        # MFA: keep minimum
        if mfa_min is None:
            mfa_min = ds["Minimum_flash_area"].values.astype(np.uint32)
        else:
            mfa_min = np.minimum(mfa_min, 
ds["Minimum_flash_area"].values.astype(np.uint32))

        ds.close()

    # Convert back to int16
    fed_out = np.clip(fed_sum, 0, 65535).astype(np.uint16)
    toe_out = np.clip(toe_sum, 0, 65535).astype(np.uint16)
    mfa_out = np.clip(mfa_min, 0, 65535).astype(np.uint16)

    # print(f"FED min-max: {fed_out.min()}–{fed_out.max()}, 
nonzero={np.count_nonzero(fed_out)}")
    # print(f"TOE min-max: {toe_out.min()}–{toe_out.max()}, 
nonzero={np.count_nonzero(toe_out)}")
    # print(f"MFA min-max: {mfa_out.min()}–{mfa_out.max()}, 
nonzero={np.count_nonzero(mfa_out)}")

    # Get center time and attributes
    ds_mid = xr.open_dataset(window[len(window) // 2], decode_cf=False)
    time_val = ds_mid["time"].values
    time_attrs = dict(ds_mid["time"].attrs)
    ds_mid.close()

    # Derive start/end times from filename tokens (preferred) or fall back to 
file time
    s_tok = re.search(r"s(\d{13})", window[0].name)
    e_tok = re.search(r"e(\d{13})", window[-1].name)
    if s_tok:
        start_dt = datetime.strptime(s_tok.group(1), "%Y%j%H%M%S")
    else:
        ds_start = xr.open_dataset(window[0], decode_cf=False)
        start_dt = dt(2017, 1, 1) + 
timedelta(seconds=int(ds_start["time"].values))
        ds_start.close()

    if e_tok:
        end_dt = datetime.strptime(e_tok.group(1), "%Y%j%H%M%S")
    else:
        ds_end = xr.open_dataset(window[-1], decode_cf=False)
        end_dt = dt(2017, 1, 1) + timedelta(seconds=int(ds_end["time"].values))
        ds_end.close()

    # Extract attributes only for the 3 vars we keep
    fed_attrs = dict(ds_template["Flash_extent_density"].attrs)
    toe_attrs = dict(ds_template["Total_Optical_energy"].attrs)
    mfa_attrs = dict(ds_template["Minimum_flash_area"].attrs)
    proj_attrs = dict(ds_template["goes_imager_projection"].attrs)
    y_attrs = dict(ds_template.coords["y"].attrs)
    x_attrs = dict(ds_template.coords["x"].attrs)

    # Update FED valid_max for 5-minute sum (5 files × max 50 = 250)
    # fed_attrs["valid_max"] = np.int16(50)
    fed_attrs.pop("valid_min", None)
    fed_attrs.pop("valid_max", None)
    fed_attrs["standard_name"] = "Flash Extent Density Five Minute Accumulation"
    fed_attrs["long_name"] = "GLM Flash Extent Density Five Minute Accumulation"

    # Build minimal L3 dataset with ONLY the 3 gridded variables
    # Use np.int32(0) for projection to ensure 32-bit int (not 64-bit)
    out = xr.Dataset(
        coords={
            "y": (("y",), ds_template.coords["y"].values, y_attrs),
            "x": (("x",), ds_template.coords["x"].values, x_attrs),
        },
        data_vars={
            "time": ((), time_val, time_attrs),
            "Flash_extent_density": (("y", "x"), fed_out, fed_attrs),
            "Total_Optical_energy": (("y", "x"), toe_out, toe_attrs),
            "Minimum_flash_area": (("y", "x"), mfa_out, mfa_attrs),
            "goes_imager_projection": ((), np.int32(0), proj_attrs),
        },
        attrs=ds_template.attrs,
    )

    # Rename variables before saving
    out = out.rename({
        "Flash_extent_density": "Flash_extent_density_w5u1",
        "Total_Optical_energy": "Total_Optical_energy_w5u1",
        "Minimum_flash_area": "Minimum_flash_area_w5u1",
    })

    # Update metadata with proper time coverage
    out.attrs["title"] = "GLM L3 Lightning Detection Gridded Product"
    
    # Set time coverage attributes
    out.attrs["time_coverage_start"] = start_dt.strftime("%Y-%m-%dT%H:%M:%SZ")
    out.attrs["time_coverage_end"] = end_dt.strftime("%Y-%m-%dT%H:%M:%SZ")

    # Build filename (L2 > L3)
    base = re.sub(r"_s\d+.*", "", window[0].name)
    base = base.replace("-L2-", "-L3-")
    s_tok = re.search(r"s(\d+)", window[0].name)
    e_tok = re.search(r"e(\d+)", window[-1].name)
    c_tok = re.search(r"c(\d+)", window[len(window) // 2].name)
    s = s_tok.group(1)
    e = e_tok.group(1)
    c = c_tok.group(1)
    filename = f"{base}_s{s}_e{e}_c{c}.nc"

    # Encoding: zlib compression for gridded vars, correct dtypes for all
    encoding = {
        "Flash_extent_density_w5u1": {"dtype": "int16", "zlib": True, 
"complevel": 6},
        "Total_Optical_energy_w5u1": {"dtype": "int16", "zlib": True, 
"complevel": 6},
        "Minimum_flash_area_w5u1": {"dtype": "int16", "zlib": True, 
"complevel": 6},
        "y": {"dtype": "int16"},
        "x": {"dtype": "int16"},
        "time": {"dtype": "int32"},
        "goes_imager_projection": {"dtype": "int32"},
    }

    # Write
    out_path = L3_DIR / filename
    out.to_netcdf(out_path, encoding=encoding)
    
    # file_size = out_path.stat().st_size / 1024
    # print(f"Wrote {out_path}")
    # print(f"  Time coverage: {out.attrs['time_coverage_start']} to 
{out.attrs['time_coverage_end']}")
    # print(f"  File size: {file_size:.1f} KB")
    
    ds_template.close()


if __name__ == "__main__":
    main()