NEDAS.datasets.vort3d package

class NEDAS.datasets.vort3d.Vort3DObs(**kwargs)[source]

Bases: SyntheticObs

Synthetic obs for the vort3d model, mirroring vort2d_obs.Vort2DObs.

Only the boundary-layer wind (‘wind_b’, i.e. ub/vb) is used for both the plain point-wind obs type and the vortex diagnostic obs operators below – it’s the layer most directly comparable to a real TC’s near-surface circulation, and the same field already used for track/intensity/size diagnostics throughout the vort3d alignment-testbed and tutorial notebook work. vort3d’s other state variables (free-atmosphere layers, theta, q, pstar) are NOT wired up here; add them the same way if/when needed.

The vortex_position/vortex_size algorithms below are deliberately byte-identical to Vort2DObs’s own (same box-summed-vorticity search, same wind-speed-based size definition) – this is not a coincidence or copy-paste laziness: the vort3d alignment-testbed scripts and the vort3d tutorial notebook already reuse Vort2DObs.vortex_position/vortex_size directly (via Vort2DObs.__new__(Vort2DObs), since they’re pure functions of (u, v) with no vort2d-specific state) for exactly this model, and all existing validated results were produced with this exact algorithm. Duplicating it here (rather than only NOW factoring out a shared module) keeps this dataset’s own results consistent with everything already run, without touching the working vort2d_obs.py.

vortex_intensity is the one deliberate departure: Vort2DObs’s own version is a domain-global max(|wind|), which silently reports the wrong vortex’s intensity whenever a second (spurious or genuinely distinct) vorticity feature anywhere in the domain happens to be windier than the tracked one – observed concretely in a 2026-07-22 IC-perturbation sensitivity sweep, where a Rmw_sprd-perturbed member’s compact vortex was sometimes out-competed in the position search too (a separate, already-known failure mode of the fixed-size search box), but a global-max intensity would have papered over that failure silently instead of surfacing it. Here, intensity is a local max within a box around the already-found vortex_position center (same box convention as vortex_size), so it reports the tracked vortex’s own intensity or nan/ garbage-but-visibly-so if the position search itself failed – not some other feature’s wind.

network_type: str
obs_range: float
core_bias_scale: float | None = None
core_bias_fraction: float = 1.0
zmin: float | None = None
zmax: float | None = None
z_units: str = 'hPa'
z_dist: str = 'uniform'
z_lambda: float = 3.0
generate_obs_network(**kwargs)[source]

Generate a random observing network for use in synthetic observation experiments.

Parameters:

**kwargs

vortex_position(u, v, first_guess=None, search_radius=20, vort_threshold_frac=0.5, cyclic_dim='x', proximity_sigma=8.0, debug=False)[source]

Vorticity-centroid center search, anchored to a first-guess position.

Two problems with the original discrete box-summed-vorticity argmax (still used below only to bootstrap a first guess when none is given): (a) it can jump to a stronger, unrelated vorticity feature anywhere else in the domain –

observed concretely in a 2026-07-22 IC-perturbation sensitivity sweep, where a few Rmw_sprd-perturbed members’ compact vortices were passed over in favor of an unrelated Vbg-driven feature, producing 900+ km single-cycle position “jumps”;

  1. even when it stays on the right feature, picking a single integer grid cell as “the” center every cycle is sensitive to grid-scale vorticity noise, producing visible frame-to-frame jitter/zigzag in ensemble track spaghetti plots that isn’t real vortex motion.

Fix, following the standard TC-tracking approach: given first_guess=(ci, cj) (typically the previous timestep’s own found center – see find_track’s chaining in vort3d/diagnostics.py), restrict the search to a search_radius-grid-cell window around it (fixes (a)), then take the vorticity-weighted CENTROID of cyclonic vorticity exceeding vort_threshold_frac of the window’s peak (not a single-cell argmax) as the center (fixes (b) – averaging over many grid cells largely cancels grid-scale noise, and thresholding first excludes the window’s own weak background clutter from the centroid). With first_guess=None (e.g. the very first timestep of a track), a coarse whole-domain box-sum argmax bootstraps a first guess, which is then itself centroid-refined the same way.

Periodic-x wrap: the search window is anchored at first_guess and wraps around the cyclic x boundary whenever the vortex is within search_radius of it. The centroid is therefore averaged over the window’s CONTINUOUS offsets and only then shifted back to grid coordinates (and taken mod nx). Averaging over the wrapped grid-column indices instead biases the result to a bogus mid-domain value whenever vorticity weight straddles the wrap – the tracker then chases that bogus center, loses the vortex, and stays frozen at the (now wrong) first guess on every later call (observed: vort3d pool member 154 pinning at a fixed cell as its vortex crossed x=0, 2026-08-03).

debug=True prints diagnostics for the wrap case: when the window straddles the cyclic boundary (weight on both sides), and when the window contains no coherent cyclonic vorticity and the first guess is returned unchanged (the stuck signature).

cyclic_dim is the model grid’s cyclic dimension(s), same convention as Grid2DBase.cyclic_dim (‘x’ for vort3d: periodic x, rigid wall y). The search window is built honoring these boundary conditions – cyclic dims wrap around the domain; non-cyclic (rigid-wall) dims reflect-pad the vorticity so a window touching the wall stays symmetric and the centroid is not biased toward the interior (the 2026-08-03 y-wall fix, generalized here to any cyclic_dim). The centroid is always averaged over the window’s continuous offsets (one coherent coordinate frame), then mapped back to grid coordinates per dimension: mod n for cyclic dims, clipped for non-cyclic dims.

proximity_sigma (grid cells, default 8): the vorticity weight is additionally multiplied by a Gaussian exp(-d^2/2 sigma^2) centred on the first guess, so the centroid stays locked onto the vortex that was anchored (the feature being tracked) rather than being pulled to unrelated features elsewhere in the search window. This is what lets the main vortex be traced through its full life cycle: when it weakens (vmax down to ~10 m/s) and its vorticity becomes comparable to background/split-off blobs, the nearby anchored feature still dominates the centroid; and near a rigid wall the reflected (mirror) part of the window is far from the anchor and down- weighted, preventing the track from being pulled onto the wall itself (observed: tracks jumping to y=nx-1 as vortices approached the north wall, 2026-08-03).

vortex_intensity(u, v, center_i, center_j, box=11)[source]

local max wind speed within a box around the vortex center (NOT a domain-global max – see class docstring for why).

vortex_size(u, v, center_i, center_j)[source]
get_wind_b(**kwargs)[source]
get_wind_b_obs(**kwargs)[source]

wind_b obs operator (registered in self.obs_operator, 2026-07-24 – previously missing entirely: ‘wind_b’ matched neither a model.variables name (it’s a dataset- level name, distinct from the model’s own ‘wind’ state variable) nor an obs_operator entry, so state_to_obs raised “unable to obtain obs prior for ‘wind_b’” for any actual wind_b assimilation attempt – get_wind_b itself was only ever called internally by generate_obs_network and the vortex_* operators’ own center-finding, which need the FULL 2D field, not point values).

Interpolates the full boundary-layer wind field (from get_wind_b) to the scattered obs locations kwargs[‘x’]/[‘y’] – the same horizontal interpolation Obs.horizontal_interp does for the generic model.variables path, done directly here since only grid (not the full Context) is available inside an obs_operator call.

get_vortex_position(**kwargs)[source]
get_vortex_intensity(**kwargs)[source]
get_vortex_size(**kwargs)[source]

Submodules