Nonlinear QC in JEDI vs GSI - TerrenceMcGuinness-NOAA/global-workflow GitHub Wiki

Nonlinear QC in JEDI vs GSI — Where it Lives in the Code

⚠ 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-rag MCP tooling against the local gdas.cd/sorc/{ufo,oops} and gsi_enkf.fd submodule trees. The v2 page corrects three substantive errors in this draft:

  1. 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 by pcgsoi.f90's varqc_iter homotopy 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.
  2. UFO does carry a live Huber-norm-like filter (atms_n20_gfs_HofX_qc_huber.yaml) implemented via ObsFunction/ObsErrorModelRamp, used extensively in NOAA's JCB templates. The v1 "zero hits for Huber" search missed this.
  3. Line-number anchors for CostJo, CostFct4DVar, CostFct3DVar, and the LinVarCha_ references have drifted in the current develop tree; LinearVariableChange has 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.


(Original v1 content follows)

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/ufo and JCSDA/oops master 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.

Context

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.

Terminology note: "VarQC" — and why it does not apply here

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_o is 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/ufo and JCSDA/oops master for the strings VarQC, "Variational QC", "variational quality control", and Huber returns 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.

Short answer

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.

What exists in JCSDA/ufo (the QC side)

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.

Where the inner-loop linearity is enforced (JCSDA/oops)

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) instantiates ObserversTLAD_ only on the trajectory.
    • computeCostTL(...) (~L294) calls obstlad_->finalizeTL(...) — purely TL.
    • computeCostAD(...) (~L307) calls obstlad_->initializeAD(...) — purely AD.
  • src/oops/base/ObserverTLAD.h — holds LinearObsOperator_ hoptlad_ (the linearized H).
  • src/oops/interface/LinearObsOperator.hsetTrajectory, simulateObsTL, simulateObsAD. The contract on ~L73 explicitly says TL/AD are linearized around the trajectory passed to setTrajectory.
  • src/oops/assimilation/CostFct4DVar.h doLinearize(...) (~L204) and CostFct3DVar.h doLinearize(...) (~L178) — these are the outer-loop re-linearization points; the inner solver only sees TL/AD afterwards.

The increment-grid → model-grid → obs-space chain in code

The exact chain described in the original question is assembled here:

  • Increment grid → model grid: LinVarCha_ via inc2model_->changeVarTL(...) and changeVarTraj(...) in CostFct4DVar.h and CostFctWeak.h.
  • Model grid → obs space (linear): LinearObsOperator<OBS>::simulateObsTL in LinearObsOperator.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.

Math summary

In both GSI and JEDI, nonlinear QC enters the variational problem only via the outer loop:

  1. 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)}.$$
  2. 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)$$ where H is the tangent-linear chain LinVarCha ∘ H_TL(trajectory) and W (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.

Bottom line

  • Implemented in JEDI public stack: outer-loop probabilistic / Bayesian QC (BayesianBackgroundCheck, ProbabilityGrossErrorWholeReport). Same role as GSI nonlinear QC; executed on H(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/AD only calls LinearObsOperator::simulateObsTL/AD.

Source verification: code paths confirmed via direct GitHub text search of JCSDA/ufo and JCSDA/oops master, May 2026.

⚠️ **GitHub.com Fallback** ⚠️