Nonlinear QC in JEDI vs GSI - TerrenceMcGuinness-NOAA/global-workflow GitHub Wiki
⚠ SUPERSEDED — see Nonlinear-QC-in-JEDI-vs-GSI-v2
This page is retained for history. A corrected v2 re-analysis was produced on 2026-05-07 using Claude Opus 4.7 with the
agentcore-mcp-ragMCP tooling against the localgdas.cd/sorc/{ufo,oops}andgsi_enkf.fdsubmodule trees. The v2 page corrects three substantive errors in this draft:
- GSI does apply a true variational QC (four variants: Andersson– Järvinen, Purser tanh, Huber mix, logistic) inside the inner loop via
vqc_int.f90, driven bypcgsoi.f90'svarqc_iterhomotopy ramp. The v1 claim that "both GSI and JEDI approximate nonlinear QC by outer-loop re-evaluation plus a linear inner loop" is wrong for GSI.- UFO does carry a live Huber-norm-like filter (
atms_n20_gfs_HofX_qc_huber.yaml) implemented viaObsFunction/ObsErrorModelRamp, used extensively in NOAA's JCB templates. The v1 "zero hits for Huber" search missed this.- Line-number anchors for
CostJo,CostFct4DVar,CostFct3DVar, and theLinVarCha_references have drifted in the currentdeveloptree;LinearVariableChangehas been refactored out of the cost function classes into its own standalone header.Please read the v2 page for the authoritative version. Everything below is preserved as originally written.
Attribution: Drafted by GitHub Copilot (Claude Opus 4.7) in collaboration with Terry McGuinness. Code-location claims were verified via direct text search of the public
JCSDA/ufoandJCSDA/oopsmaster branches on 2026-05-07 using the EIB MCP-RAG tooling. Any errors in interpretation are the human author's; please open an issue or PR if you spot one.
A discussion question came up:
Is the current nonlinear QC (in GSI) implemented in JEDI? In the current JEDI implementation there are no nonlinear observation operators defined on the increment grids. The linear observation operator and its adjoint used in the inner loops are obtained from the chain of linear transformations between increment grids and model grids and the linear observation operators on model grids. Jim's counter-point: if nonlinear QC is implemented, it should mean nonlinear observation operators on the increment grid.
This page resolves that question by pointing to concrete files in the public JCSDA JEDI stack.
An earlier draft of this discussion claimed that "JEDI/UFO implements the Variational Quality Control (VarQC) framework." That statement was incorrect and has been retracted. Clarification for SMEs unfamiliar with the term:
-
VarQC (Variational Quality Control) is a specific class of
algorithms — the canonical references are Andersson & Järvinen
(1999, QJRMS 125, 697–722) at ECMWF, and the Huber-norm robust
formulation used at the Met Office and elsewhere — in which the
observation cost-function term
J_ois modified inside the minimization so that the effective weight of an observation decreases continuously as its departure grows. It is not a synonym for "any nonlinear QC done in a variational system." -
What was actually verified in the JCSDA code base (May 2026): direct
text search of
JCSDA/ufoandJCSDA/oopsmaster for the stringsVarQC,"Variational QC","variational quality control", andHuberreturns zero hits. There is no Huber-norm or Andersson-Järvinen-style cost-function modification implemented in the public JEDI stack today. -
What JEDI does ship is a Bayesian / probability-of-gross-error
family of QC filters (see the table below), which serves the same
operational role as GSI's nonlinear QC — outer-loop re-evaluation of
observation acceptance based on departure magnitude — but does so via
hard QC-flag updates and obs-error inflation outside the
minimization, not via a Huber term inside
CostJo.
The distinction matters for Jim's question: a true VarQC term would
introduce a nonlinearity into J_o(δx) that the inner solver would have to
cope with (typically via Gauss-Newton or a quasi-Newton method). The
Bayesian-filter approach JEDI uses sidesteps that entirely by freezing the
QC decisions before the inner loop starts.
No — not in the VarQC / Huber sense, and not as a nonlinear
observation operator on the increment grid. JEDI/UFO ships QC filters that
play the same role as GSI's nonlinear QC (Bayesian /
probability-of-gross-error), but they are not implemented as a Huber-norm
reweighting term inside the variational cost function. All nonlinearity is
confined to the outer loop (trajectory). The inner loop only ever sees
the linear chain LinVarCha ∘ H_TL(trajectory).
So the original statement to Jim is correct for the inner loop. Jim's inference ("nonlinear QC ⇒ nonlinear H on the increment") is a reasonable theoretical expectation — and it is exactly what a true VarQC formulation would imply — but neither GSI nor the current JEDI code base actually does that. Both approximate nonlinear QC by outer-loop re-evaluation plus a linear inner loop.
As noted above, searches for VarQC, "Variational QC",
"variational quality control", and Huber return zero hits in
JCSDA/ufo master — so there is no explicit Huber/VarQC filter. The QC
machinery that is present, and that fills the same operational niche as
GSI's nonlinear QC, is a Bayesian / PGE family:
| File | Role |
|---|---|
src/ufo/filters/BayesianBackgroundCheck.h / .cc
|
Posterior probability-of-gross-error check on (y − H(x_b)) using a Gaussian-good + flat-bad mixture. Closest analog to GSI's nonlinear QC. |
src/ufo/filters/BayesianBackgroundQCFlags.h |
Translates updated PGEs into QC flags. |
src/ufo/filters/ProbabilityGrossErrorWholeReport.h |
PGE for a whole report (e.g. radiosonde profile). |
src/ufo/filters/BackgroundCheck.cc |
Standard threshold-based background check. |
src/ufo/filters/QCflags.h |
Defines bayesianQC = 26 returned by these filters. |
src/ufo/filters/QCmanager.h / .cc
|
Orchestrates filters; postFilter(GeoVaLs, hofx, …) is where filters see the nonlinear H(x) evaluated on the model trajectory. |
src/ufo/instantiateObsFilterFactory.h |
Filter registry — canonical list of every QC filter UFO knows about. No VarQC entry exists.
|
All of these run outside the minimization. Their applyFilter(...) /
postFilter(...) methods only modify QC flags
(ioda::ObsDataVector<int>) and obs errors
(ioda::ObsDataVector<float>). They never appear inside
CostJo::computeCostTL / computeCostAD.
This is the part that vindicates the original statement: the inner loop
never calls a nonlinear H.
-
src/oops/assimilation/CostJo.h— the cost-function term:-
setPostProcTraj(...)(~L266) instantiatesObserversTLAD_only on the trajectory. -
computeCostTL(...)(~L294) callsobstlad_->finalizeTL(...)— purely TL. -
computeCostAD(...)(~L307) callsobstlad_->initializeAD(...)— purely AD.
-
-
src/oops/base/ObserverTLAD.h— holdsLinearObsOperator_ hoptlad_(the linearized H). -
src/oops/interface/LinearObsOperator.h—setTrajectory,simulateObsTL,simulateObsAD. The contract on ~L73 explicitly says TL/AD are linearized around the trajectory passed tosetTrajectory. -
src/oops/assimilation/CostFct4DVar.hdoLinearize(...)(~L204) andCostFct3DVar.hdoLinearize(...)(~L178) — these are the outer-loop re-linearization points; the inner solver only sees TL/AD afterwards.
The exact chain described in the original question is assembled here:
- Increment grid → model grid:
LinVarCha_viainc2model_->changeVarTL(...)andchangeVarTraj(...)inCostFct4DVar.handCostFctWeak.h. - Model grid → obs space (linear):
LinearObsOperator<OBS>::simulateObsTLinLinearObsOperator.h.
There is no class anywhere in oops or ufo named
NonlinearObsOperatorOnIncrement (or anything analogous). The only place a
nonlinear H is evaluated is Observers (not ObserversTLAD), driven from
CostFunction::evaluate / runNL on the model state, never on
Increment.
In both GSI and JEDI, nonlinear QC enters the variational problem only via the outer loop:
- On the outer loop (model/trajectory grid) compute the departure
$d = y - H(x_b)$ and a QC weight (e.g. PGE-based)$$w(d) = \frac{p_{\text{Gauss}}(d)}{p_{\text{Gauss}}(d) + p_{\text{flat}}(d)}.$$ - In the inner loop the obs term is
$$J_o = \tfrac{1}{2}(H \delta x - d)^T , W R^{-1} (H \delta x - d)$$ whereHis the tangent-linear chainLinVarCha ∘ H_TL(trajectory)andW(or its hard-flag equivalent) is frozen for the duration of the inner solve. Nonlinearity re-enters only when the next outer loop re-linearizes.
A genuine nonlinear-H-on-increment scheme would require something like a
Gauss-Newton inner loop with H re-linearized inside. JEDI's architecture
supports outer-loop re-linearization, not nonlinear inner solves.
-
Implemented in JEDI public stack: outer-loop probabilistic / Bayesian
QC (
BayesianBackgroundCheck,ProbabilityGrossErrorWholeReport). Same role as GSI nonlinear QC; executed onH(x_trajectory); results frozen into QC flags / obs-error inflation before each inner loop. -
Not implemented: no Huber-VarQC term inside
CostJo, and no nonlinear obs operator on the increment.CostJo::computeCostTL/ADonly callsLinearObsOperator::simulateObsTL/AD.
Source verification: code paths confirmed via direct GitHub text search of
JCSDA/ufo and JCSDA/oops master, May 2026.