The observation database
The high-fidelity observations \(\mathbf{y}_k\) used by the analysis
phase (see Data Assimilation) are not passed to CONES directly: they
are read once at start-up from a netCDF file, whose path is set by
obsFile in The conesDict reference. This page describes the file’s expected
structure and shows two ways to build one.
Expected structure
The file is read by
conesStaticObservation
and the netCDF helpers in conesToolBox.conesFunctions
(cones_count_netcdf_obs(),
cones_get_netcdf_obs_coord()), which
impose the following schema:
Dimension |
Meaning |
|---|---|
|
Number of observation points. Required, and must be named exactly
|
|
Number of time samples available for each observed variable. |
Variable |
Meaning |
|---|---|
|
Coordinates of each observation point, shaped |
one per |
The observed value of that variable at each point and time, shaped
|
the |
The standard deviation \(\sigma_{i,k}\) of that observation,
same shape. This is what fills the observation covariance
\(\mathbf{R}_k = \mathrm{diag}(\sigma_{i,k}^2)\) (see
The conesDict reference) and the perturbation drawn by
|
Only the variables named in obsVar (plus their _std companion) are
actually read at run time; extra data variables in the file are ignored.
A CF-decoding gotcha
If you set a dtype entry directly in a variable’s .attrs (rather
than its .encoding) — for instance to force time to be interpreted
as timedelta64[ns] — some xarray versions refuse to CF-decode the file
on open. Both examples below set ds.time.attrs['dtype'] for exactly
this reason; cones_open_netcdf_dataset()
is CONES’s read-side workaround, moving any such dtype attribute into
encoding before decoding. Reuse it if you write your own reader.
Example 1: synthetic observations
utils/generate_observation_file.py builds a database of 5 point
velocity observations, constant in time — this is what the
cavity tutorial uses. Run it with the relative standard
deviation to apply to each value (5% here):
python3 utils/generate_observation_file.py 0.05
1import xarray as xr
2import numpy as np
3import sys
4
5if len(sys.argv) != 2:
6 print("Usage: python3 generate_observation_file.py <relative_std>")
7 print(" relative_std: fraction of each observed value used as its standard deviation, e.g. 0.05")
8 sys.exit(1)
9relative_std = float(sys.argv[1])
10
11# Define number of observations and coordinates
12num_obs = 5
13n_time = 101
14time = np.linspace(0, 100, n_time)
15
16obs_ids = np.arange(num_obs)
17
18x = np.array([0.0425, 0.0475, 0.0525, 0.0575, 0.0675])
19y = np.ones(num_obs)*0.0975
20z = np.ones(num_obs)*0.005
21
22# Define observations values and std
23
24u_static = np.array([0.753300, 0.799275, 0.824227, 0.851823, 0.874961])
25v_static = np.array([0.0455249, 0.0403204, 0.0357992, 0.0322402, 0.0263673])
26w_static = np.zeros(num_obs)
27
28u_std_static = u_static*relative_std
29v_std_static = v_static*relative_std
30w_std_static = w_static*relative_std
31
32# Broadcasting
33u = np.zeros((n_time, num_obs))
34v = np.zeros((n_time, num_obs))
35w = np.zeros((n_time, num_obs))
36u_std = np.zeros((n_time, num_obs))
37v_std = np.zeros((n_time, num_obs))
38w_std = np.zeros((n_time, num_obs))
39
40
41for t_idx, t in enumerate(time):
42 u[t_idx, :] = u_static
43 v[t_idx, :] = v_static
44 w[t_idx, :] = w_static
45 u_std[t_idx, :] = u_std_static
46 v_std[t_idx, :] = v_std_static
47 w_std[t_idx, :] = w_std_static
48
49
50# Assemble dataset
51
52ds = xr.Dataset(
53 data_vars={
54 "u": (["time", "obs"], u),
55 "v": (["time", "obs"], v),
56 "w": (["time", "obs"], w),
57 "u_std": (["time", "obs"], u_std),
58 "v_std": (["time", "obs"], v_std),
59 "w_std": (["time", "obs"], w_std),
60 "x": (["obs"], x),
61 "y": (["obs"], y),
62 "z": (["obs"], z),
63 # "obs_type":(["obs"], obs_types)
64 },
65 coords={
66 "time": time,
67 "obs": obs_ids,
68 }
69)
70
71# ds = ds_static.expand_dims(time=t)
72ds.time.attrs['units'] = 'nanoseconds'
73ds.x.attrs = {'axis': 'X', 'units': 'm', 'standard_name': 'projection_x_coordinate'}
74ds.y.attrs = {'axis': 'Y', 'units': 'm', 'standard_name': 'projection_y_coordinate'}
75ds.z.attrs = {'axis': 'Z', 'units': 'm', 'standard_name': 'projection_z_coordinate'}
76
77ds.attrs = {'feature_type': 'point', 'Conventions': 'CF-1.6'}
78ds.time.attrs['dtype'] = 'timedelta64[ns]'
79# Add Metadata
80# ds.u.attrs = {"units": "m/s", "standard_name": "u"}
81# ds.u_std.attrs = {"units": "m/s", "descriptions": "Standard deviation of u"}
82# ds.v.attrs = {"units": "m/s", "standard_name": "v"}
83# ds.v_std.attrs = {"units": "m/s", "descriptions": "Standard deviation of v"}
84# ds.w.attrs = {"units": "m/s", "standard_name": "w"}
85# ds.w_std.attrs = {"units": "m/s", "descriptions": "Standard deviation of w"}
86# ds.x.attrs = {"units": "m"}
87# ds.y.attrs = {"units": "m"}
88# ds.z.attrs = {"units": "m"}
89# ds.attrs = {"description": "Database of point observations over time"}
90ds.to_netcdf("observation_database.nc")
91print("File saved!")
92print(ds)
Adapt the coordinates and observed values to your own case; keep every
data variable shaped (time, obs) (or (obs,) for x/y/z)
and make sure every observed variable has its _std companion.
Example 2: observations from OpenFOAM probes
If you already have a reference/high-fidelity run, utils/probes_to_observation_netcdf.py
converts the output of OpenFOAM’s probes function object (as configured
in system/probes) into an observation database, reading the probe
coordinates from the file’s own header comments:
python3 utils/probes_to_observation_netcdf.py postProcessing/probes/0/U
It currently hardcodes num_obs = 5 and a flat 5% relative standard
deviation — open the script and adjust both to match your system/probes
setup before relying on it.