Skip to content

Commit 7e06306

Browse files
committed
This commit aligns check_prepbufr.py with the actual behavior of the GSI,
removing false structural/diagnostic flags and enforcing only checks that have real operational impact. Main changes: - Structural header: * Updated minimal GSI header to require only XOB, YOB and DHR. * TYP is no longer treated as mandatory, matching GSI real usage. - Physical ranges and units: * Reworked RANGE definitions to strictly follow PREPBUFR DX tables. * Corrected TOB to be checked in degrees Celsius (°C), not Kelvin. * Updated pressure handling (POB/PRSS) with explicit unit semantics. - Assimilable variables: * Introduced USEFUL_BY_TYPE mapping to reflect which variables are actually assimilable by GSI for each message type. * Avoids incorrectly flagging subsets as "no useful data". - Longitude handling: * Structural lon/lat checks now allow 0–360° longitude, consistent with GSI internal normalization. - Diagnostics logic: * EVN sequences (TEVN/WEVN/PEVN/ZEVN/QEVN) treated as optional (INFO only). * Missing observation errors (*_OE) treated as optional (GSI uses defaults). * Diagnostic status is raised to ATENÇÃO only for conditions with real assimilation impact: - time outside assimilation window (DHR) - physically implausible values/units - missing critical surface fields for ADPSFC - ADPSFC diagnostics: * Applied only when msg_type == ADPSFC. * PRSS and PWO treated as critical; CAT no longer required. - Documentation: * Added extensive inline documentation explaining the rationale of each structural and diagnostic decision, explicitly tied to GSI behavior. Result: - Eliminates false positives previously reported by the checker. - Status_struct now strictly reflects whether GSI can read and use the file. - Status_diag highlights only conditions with potential operational impact.
1 parent 6f38dee commit 7e06306

1 file changed

Lines changed: 213 additions & 52 deletions

File tree

scripts/check_prepbufr.py

Lines changed: 213 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
# -----------------------------------------------------------------------------
6868

6969
# Header mínimo para o GSI
70-
REQ_HDR_GSI: Tuple[str, ...] = ("XOB", "YOB", "DHR", "TYP")
70+
# GSI REAL exige apenas posição e tempo
71+
REQ_HDR_GSI: Tuple[str, ...] = ("XOB", "YOB", "DHR")
7172

7273
# Sequências Table D para eventos (diagnóstico)
7374
REQ_SEQS: Tuple[str, ...] = ("TEVN", "WEVN", "PEVN", "ZEVN", "QEVN")
@@ -78,24 +79,42 @@
7879
# Progresso
7980
DEFAULT_PROGRESS_EVERY: int = 10_000
8081

81-
# Faixas de plausibilidade (diagnóstico)
82+
# Faixas de plausibilidade baseadas estritamente na tabela PREPBUFR
83+
8284
RANGE = {
83-
"XOB": (-180.0, 180.0),
84-
"YOB": (-90.0, 90.0),
85-
"TOB": (180.0, 340.0),
86-
"POB": (1.0, 1100.0),
87-
"ZOB": (-500.0, 40000.0),
88-
"UOB": (-120.0, 120.0),
89-
"VOB": (-120.0, 120.0),
90-
"QOB": (0.0, 0.05),
91-
"PRSS": (500.0, 1085.0),
92-
"PWO": (0.0, 100.0),
93-
"DHR": (-6.0, 6.0),
94-
"HOVI": (0.0, 100000.0),
95-
"TDO": (173.0, 333.0),
96-
"MSST": (271.0, 313.0),
85+
# Geolocalização
86+
"XOB": (-180.0, 180.0), # DEG E
87+
"YOB": (-90.0, 90.0), # DEG N
88+
89+
# Tempo
90+
"DHR": (-24.0, 24.0), # HOURS (tabela permite até ±24h)
91+
92+
# Temperatura (DEG C)
93+
"TOB": (-90.0, 60.0), # Atmosfera (°C)
94+
"TDO": (-100.0, 40.0), # Ponto de orvalho (°C)
95+
96+
# Temperatura de superfície (K)
97+
"TMSK": (200.0, 350.0), # K (compatível com tabela)
98+
99+
# Pressão
100+
"POB": (1.0, 1100.0), # mb
101+
"PRSS": (50000.0, 108500.0), # Pa (ATENÇÃO: Pascals!)
102+
103+
# Altura
104+
"ZOB": (-500.0, 40000.0), # m (ref = -1000 m)
105+
106+
# Vento
107+
"UOB": (-150.0, 150.0), # m/s
108+
"VOB": (-150.0, 150.0), # m/s
109+
110+
# Umidade específica
111+
"QOB": (0.0, 30000.0), # mg/kg (0–30 g/kg)
112+
113+
# Água precipitável
114+
"PWO": (0.0, 120.0), # mm (valores extremos tropicais)
97115
}
98116

117+
99118
# Unidade de pressão (para interpretar POB/PRSS) e autodetecção
100119
PRESSURE_UNITS = ("hpa", "cb", "pa", "auto")
101120
DEFAULT_PRESSURE_UNIT = "hpa"
@@ -107,9 +126,29 @@
107126
)
108127

109128
# Limites estruturais para lon/lat (mínimo GSI)
129+
# GSI aceita lon 0–360 e normaliza internamente
110130
LON_RANGE_STRUCT = (-180.0, 360.0)
111131
LAT_RANGE_STRUCT = (-90.0, 90.0)
112132

133+
# Variáveis assimiláveis por tipo de mensagem (Tabela A / comportamento GSI)
134+
USEFUL_BY_TYPE = {
135+
"ADPUPA": ("TOB", "QOB", "UOB", "VOB", "ZOB"),
136+
"AIRCAR": ("TOB", "UOB", "VOB"),
137+
"AIRCFT": ("TOB", "UOB", "VOB"),
138+
"SATWND": ("UOB", "VOB"),
139+
"PROFLR": ("UOB", "VOB"),
140+
"VADWND": ("UOB", "VOB"),
141+
"ADPSFC": ("PRSS", "TOB", "QOB", "UOB", "VOB"),
142+
"SFCSHP": ("PRSS", "TOB", "QOB", "UOB", "VOB"),
143+
# BUOY: GSI aceita só vento, só pressão ou só temperatura
144+
"SFCSHP": ("PRSS", "TOB", "QOB", "UOB", "VOB", "POB"),
145+
"SATEMP": ("TMBR",),
146+
"GOESND": ("TMBR",),
147+
"SPSSMI": ("PWO", "UOB", "VOB"),
148+
"QKSWND": ("UOB", "VOB"),
149+
}
150+
151+
113152
# Missing típico da BUFRLIB
114153
try:
115154
BUFR_MISSING = float(ncepbufr.bufrlib.getbmiss())
@@ -299,7 +338,7 @@ def _autodetect_pressure_unit(path: str, max_samples: int = PRESSURE_AUTODETECT_
299338
cnt = 0
300339
while b.advance() == 0 and cnt < max_samples:
301340
while b.load_subset() == 0 and cnt < max_samples:
302-
for nm in ("POB", "PRSS"):
341+
for nm in ("POB",):
303342
arr = b.read_subset(nm)
304343
if getattr(arr, "size", 0):
305344
try:
@@ -477,8 +516,8 @@ def inc(d: Dict[object, int], k: object, by: int = 1):
477516
where_gsi_lonlat.append(_fmt_where(mtyp, nsub, xob, yob, dhr))
478517
fail_records.append(FailRecord(_b2s(mtyp), nsub, FAIL_GSI_LONLAT, None))
479518

480-
# Pelo menos uma variável "útil" (TOB/QOB/U/V/POB/ZOB/PRSS)
481-
useful_mnems = ("TOB", "QOB", "UOB", "VOB", "POB", "ZOB", "PRSS")
519+
# Pelo menos uma variável "útil" (dirigido por tipo)
520+
useful_mnems = USEFUL_BY_TYPE.get(_b2s(mtyp), ())
482521
has_useful = False
483522
for nm in useful_mnems:
484523
arr = _read1(read, nm)
@@ -508,6 +547,7 @@ def inc(d: Dict[object, int], k: object, by: int = 1):
508547
# -------------------------------
509548

510549
# 2.1. Eventos
550+
# NOTA: Eventos EVN são opcionais no GSI real → ignorados
511551
seq_ok = False
512552
subset_no_events = False
513553

@@ -583,6 +623,7 @@ def check_range(name: str) -> bool:
583623
subset_miss_oberrs = False
584624
if check_oberrs:
585625
pairs = [
626+
# *_OE são opcionais no GSI (erro default)
586627
("TOB", "TOE"),
587628
(("UOB", "VOB"), "WOE"),
588629
("POB", "POE"),
@@ -604,12 +645,12 @@ def check_range(name: str) -> bool:
604645

605646
# 2.5. ADPSFC (diagnóstico, se --kind adpsfc)
606647
subset_miss_adpsfc = False
607-
if kind == "adpsfc":
648+
# Diagnóstico ADPSFC só se msg_type == ADPSFC
649+
if kind == "adpsfc" and _b2s(mtyp) == "ADPSFC":
608650
if not subset_gsi_hdr_bad: # só olha ADPSFC se header básico ok
609651
prss = _read1(read, "PRSS")
610652
pwo = _read1(read, "PWO")
611-
cat = _read1(read, "CAT")
612-
if (prss is None) or (pwo is None) or (cat is None):
653+
if (prss is None) or (pwo is None):
613654
subset_miss_adpsfc = True
614655
else:
615656
try:
@@ -637,54 +678,174 @@ def check_range(name: str) -> bool:
637678
if pbar is not None:
638679
pbar.close()
639680

681+
# -------------------------------------------------------------------------
640682
# Status estrutural (mínimo GSI)
683+
#
684+
# Objetivo:
685+
# Determinar se o arquivo PREPBUFR é ESTRUTURALMENTE UTILIZÁVEL pelo GSI,
686+
# isto é, se o GSI consegue LER, INTERPRETAR e TENTAR ASSIMILAR os dados.
687+
#
688+
# Critérios adotados (estritamente alinhados ao GSI real):
689+
#
690+
# - gsi_bad_hdr:
691+
# Subsets sem header essencial (XOB, YOB, DHR, TYP).
692+
# -> Sem essas chaves, o GSI NÃO consegue sequer posicionar a observação.
693+
#
694+
# - gsi_bad_lonlat:
695+
# Longitude ou latitude claramente inválidas.
696+
# -> O GSI rejeita observações fora de limites geográficos básicos.
697+
#
698+
# - gsi_no_useful:
699+
# Subsets sem NENHUMA variável potencialmente assimilável
700+
# (TOB, QOB, UOB, VOB, POB, ZOB ou PRSS).
701+
# -> Observação sem conteúdo útil não entra no sistema.
702+
#
703+
# Regra:
704+
# - status_struct = "OK"
705+
# TODOS os critérios acima são satisfeitos para TODOS os subsets.
706+
#
707+
# - status_struct = "PROBLEMAS_ESTRUTURAIS"
708+
# Pelo menos um subset falhou em algum critério estrutural.
709+
#
710+
# Observação importante:
711+
# Este status define o código de saída do programa:
712+
# - 0 → OK (arquivo utilizável pelo GSI)
713+
# - 1 → PROBLEMAS_ESTRUTURAIS (arquivo inadequado para o GSI)
714+
# -------------------------------------------------------------------------
641715
if gsi_bad_hdr == 0 and gsi_bad_lonlat == 0 and gsi_no_useful == 0:
642716
status_struct = "OK"
643717
else:
644718
status_struct = "PROBLEMAS_ESTRUTURAIS"
645719

646-
# Status diagnóstico: ATENÇÃO se houver qualquer contador > 0
720+
# -------------------------------------------------------------------------
721+
# Status diagnóstico
722+
#
723+
# Objetivo:
724+
# Elevar status_diag para "ATENÇÃO" apenas quando houver condições
725+
# que possam causar REJEIÇÃO da observação ou IMPACTO OPERACIONAL REAL
726+
# no GSI.
727+
#
728+
# Princípios adotados (alinhados ao comportamento do GSI):
729+
#
730+
# - diag_no_events:
731+
# -> EVN ausente (TEVN/WEVN/PEVN/ZEVN/QEVN)
732+
# -> NÃO eleva status.
733+
# -> O GSI não exige eventos para leitura ou assimilação.
734+
#
735+
# - diag_miss_oberrs:
736+
# -> Erros de observação ausentes (*_OE)
737+
# -> NÃO eleva status.
738+
# -> O GSI utiliza erros default definidos em convinfo.
739+
#
740+
# - diag_time_oow:
741+
# -> DHR fora da janela temporal
742+
# -> ELEVA status (ATENÇÃO).
743+
# -> Observações fora da janela podem ser descartadas pelo GSI.
744+
#
745+
# - diag_bad_units:
746+
# -> Valores ou unidades fisicamente implausíveis
747+
# -> ELEVA status (ATENÇÃO).
748+
# -> Podem causar rejeição posterior ou contaminar a análise.
749+
#
750+
# - diag_miss_adpsfc:
751+
# -> Inconsistência em campos críticos de superfície (PRSS/PWO/CAT)
752+
# -> ELEVA status (ATENÇÃO).
753+
# -> PRSS é essencial para observações de superfície no GSI.
754+
#
755+
# Resultado:
756+
# - status_diag = "OK"
757+
# Nenhuma condição com impacto real detectada (diagnóstico informativo).
758+
# - status_diag = "ATENÇÃO"
759+
# Existe pelo menos uma condição com potencial impacto na assimilação.
760+
# -------------------------------------------------------------------------
647761
if any([
648-
diag_no_events,
649-
diag_time_oow,
650-
diag_bad_units,
651-
diag_miss_oberrs,
652-
diag_miss_adpsfc,
762+
diag_time_oow, # DHR fora da janela temporal
763+
diag_bad_units, # valores/unidades fisicamente implausíveis
764+
diag_miss_adpsfc, # campos críticos ausentes para ADPSFC
653765
]):
654766
status_diag = "ATENÇÃO"
655767
else:
656768
status_diag = "OK"
657769

770+
# -------------------------------------------------------------------------
771+
# Consolidação do resumo final da validação
772+
#
773+
# O objeto Summary concentra:
774+
# - métricas estruturais mínimas (relevantes ao GSI)
775+
# - métricas diagnósticas (informativas e/ou de ATENÇÃO)
776+
# - status final (estrutural e diagnóstico)
777+
# - amostras "onde" ocorreram os problemas
778+
# - registros detalhados de falhas (para CSV detalhado)
779+
# -------------------------------------------------------------------------
658780
summary = Summary(
659-
nmsg=nmsg,
660-
nsub=nsub,
661-
gsi_bad_hdr=gsi_bad_hdr,
662-
gsi_bad_lonlat=gsi_bad_lonlat,
663-
gsi_no_useful=gsi_no_useful,
664-
per_type_total=per_type_total,
665-
per_type_gsi_bad_hdr=per_type_gsi_bad_hdr,
666-
per_type_gsi_bad_lonlat=per_type_gsi_bad_lonlat,
667-
per_type_gsi_no_useful=per_type_gsi_no_useful,
668-
per_type_gsi_approved=per_type_gsi_approved,
669-
diag_no_events=diag_no_events,
670-
diag_time_oow=diag_time_oow,
671-
diag_bad_units=diag_bad_units,
672-
diag_miss_oberrs=diag_miss_oberrs,
673-
diag_miss_adpsfc=diag_miss_adpsfc,
781+
# ---------------------------------------------------------------------
782+
# Totais globais
783+
# ---------------------------------------------------------------------
784+
nmsg=nmsg, # número total de mensagens BUFR no arquivo
785+
nsub=nsub, # número total de subsets processados
786+
787+
# ---------------------------------------------------------------------
788+
# Métricas estruturais (mínimo GSI)
789+
# Estas métricas indicam se o GSI CONSEGUE ler e usar o PREPBUFR.
790+
# Qualquer uma delas diferente de zero implica status_struct != OK.
791+
# ---------------------------------------------------------------------
792+
gsi_bad_hdr=gsi_bad_hdr, # subsets sem header essencial (XOB/YOB/DHR/TYP)
793+
gsi_bad_lonlat=gsi_bad_lonlat, # lon/lat claramente inválidos (checagem grosseira)
794+
gsi_no_useful=gsi_no_useful, # subsets sem NENHUMA variável assimilável
795+
796+
# ---------------------------------------------------------------------
797+
# Contadores por tipo de mensagem (msg_type)
798+
# Permitem avaliar qualidade e cobertura por tipo (ADPSFC, SFCSHP, etc.)
799+
# ---------------------------------------------------------------------
800+
per_type_total=per_type_total, # total de subsets por msg_type
801+
per_type_gsi_bad_hdr=per_type_gsi_bad_hdr, # header inválido por msg_type
802+
per_type_gsi_bad_lonlat=per_type_gsi_bad_lonlat, # lon/lat inválido por msg_type
803+
per_type_gsi_no_useful=per_type_gsi_no_useful, # sem variáveis úteis por msg_type
804+
per_type_gsi_approved=per_type_gsi_approved, # subsets estruturalmente OK (GSI)
805+
806+
# ---------------------------------------------------------------------
807+
# Métricas diagnósticas (não impedem leitura pelo GSI)
808+
# Servem para QA/QC, debug e avaliação da cadeia de produção.
809+
# ---------------------------------------------------------------------
810+
diag_no_events=diag_no_events, # EVN ausente (TEVN/WEVN/PEVN/ZEVN/QEVN)
811+
diag_time_oow=diag_time_oow, # DHR fora da janela temporal
812+
diag_bad_units=diag_bad_units, # valores/unidades fisicamente implausíveis
813+
diag_miss_oberrs=diag_miss_oberrs, # erros de observação (*_OE) ausentes
814+
diag_miss_adpsfc=diag_miss_adpsfc, # inconsistência em PRSS/PWO/CAT (superfície)
815+
816+
# ---------------------------------------------------------------------
817+
# Métricas diagnósticas por tipo de mensagem
818+
# Úteis para identificar problemas sistemáticos por origem/tipo.
819+
# ---------------------------------------------------------------------
674820
per_type_no_events=per_type_no_events,
675821
per_type_time_oow=per_type_time_oow,
676822
per_type_bad_units=per_type_bad_units,
677823
per_type_miss_oberrs=per_type_miss_oberrs,
678824
per_type_miss_adpsfc=per_type_miss_adpsfc,
679-
status_struct=status_struct,
680-
status_diag=status_diag,
681-
pressure_unit_used=pressure_unit_used,
682-
where_gsi_hdr=where_gsi_hdr,
683-
where_gsi_lonlat=where_gsi_lonlat,
684-
where_gsi_no_useful=where_gsi_no_useful,
685-
where_events=where_events,
686-
where_time=where_time,
687-
where_units=where_units,
825+
826+
# ---------------------------------------------------------------------
827+
# Status finais
828+
# ---------------------------------------------------------------------
829+
status_struct=status_struct, # OK / PROBLEMAS_ESTRUTURAIS
830+
status_diag=status_diag, # OK / ATENÇÃO (conforme regras definidas)
831+
pressure_unit_used=pressure_unit_used, # unidade de pressão adotada (hpa/cb/pa)
832+
833+
# ---------------------------------------------------------------------
834+
# Exemplos ("onde") para facilitar inspeção manual
835+
# Limitados por --where
836+
# ---------------------------------------------------------------------
837+
where_gsi_hdr=where_gsi_hdr, # exemplos de header estrutural inválido
838+
where_gsi_lonlat=where_gsi_lonlat, # exemplos de lon/lat inválidos
839+
where_gsi_no_useful=where_gsi_no_useful, # exemplos sem variáveis úteis
840+
where_events=where_events, # exemplos sem EVN
841+
where_time=where_time, # exemplos fora da janela temporal
842+
where_units=where_units, # exemplos com unidades implausíveis
843+
844+
# ---------------------------------------------------------------------
845+
# Registro detalhado de falhas
846+
# Cada entrada representa uma falha específica em um subset.
847+
# Base para --report-csv
848+
# ---------------------------------------------------------------------
688849
fail_records=fail_records,
689850
)
690851

0 commit comments

Comments
 (0)