Synthetic observations via perfect_model_obs#
Warning
Under development.
PerfectModelSource generates synthetic observations by
running DART’s perfect_model_obs executable. For each assimilation window it:
Builds a template
obs_seq.infrom a user-defined observing network.Patches
input.nmlwith the obs_seq filenames and window time bounds.Runs
perfect_model_obsin an isolated per-window directory.Returns the resulting
obs_seq.outas the window’s obs file.
The caller uses it identically to CrocLakeSource or NNJASource — only
the source object changes.
Prerequisites#
pmo_run_dir must contain:
The compiled
perfect_model_obsexecutableA base
input.nmlwith aperfect_model_obs_nmlblock (the source patches only the obs_seq filenames and time-bound fields)Every input file
input.nmlnames — for MOM6 that istemplate_file,static_fileandocean_geometryfrommodel_nml, plusinput_state_filesunless astate_providersupplies it per window
This is the same directory structure used to run perfect_model_obs by hand.
PerfectModelSource checks all of this at construction rather than letting
it surface as a namelist error from inside perfect_model_obs, once per
window. It reports every missing file at once, each against the namelist
entry that named it:
input.nml in '/path/to/pmo_run' names file(s) that are not there:
ocean_geometry.nc (named by model_nml:ocean_geometry)
input_files_from_nml() is the same reader, if you want
the list of files a namelist expects without building a source.
ObsNetworkEntry fields#
Field |
Type |
Description |
|---|---|---|
|
|
DART obs type name, e.g. |
|
|
Latitude in degrees |
|
|
Longitude in degrees (-180 to 180) |
|
|
Vertical coordinate value |
|
|
|
|
|
Observation error variance |
|
|
Offset from the analysis time (default |
Driving it from model output: state_provider#
perfect_model_obs cannot advance a large model, so it interpolates from a
state file you hand it. MOM6StateProvider indexes the timeslices of a MOM6
run’s output and serves one per window, and observations are then placed at
the valid time of that state, not at the analysis time.
That inverts the usual configuration: the analysis times must line up with the
model output times. Use first_analysis, not start:
config = ObsGenConfig(
first_analysis=datetime.datetime(2015, 10, 4, 12), # first model output time
end=datetime.datetime(2015, 10, 8, 12), # last analysis time
assimilation_frequency=datetime.timedelta(hours=24),
...
)
Using start here would place the first analysis one frequency after the
first model output time, and every state could fall outside every window.
A window selects the earliest state in (date0, date1] — open below, closed
above, matching the window perfect_model_obs is given (first_obs = date0 + 1s, last_obs = date1). A state landing exactly on date0 belongs to the
previous window; selecting it would place every observation one second before
first_obs and DART would abort with “All obs in sequence are before
first_obs_days:first_obs_seconds”.
PerfectModelSource.check_coverage reports the alignment before any window
runs:
Model state coverage: 1 of 5 window(s) have a state; 1 model output time(s)
(1 used, 0 shadowed, 0 outside all windows).
and raises when nothing lines up, naming the setting that would fix it:
No model state falls in any assimilation window, so no observations can be generated.
model output times: 2015-10-04T12:00:00
analysis times: 2015-10-06T00:00:00, 2015-10-07T00:00:00, 2015-10-08T00:00:00
windows cover: (2015-10-05T12:00:00, 2015-10-08T12:00:00] every 1 day, 0:00:00
Observations are placed at the model state's valid time, so the analysis
times must line up with the model output times. Set
first_analysis=2015-10-04T12:00:00 and end>=2015-10-04T12:00:00 to cover
the model output.
It also flags shadowed states (a second state in a window that already has an earlier one — only the earliest is used) and off-centre states (in a window but not at its analysis time, so obs land away from the window centre).
Controlling observation time within a window#
With a state_provider, observations are placed at the state’s valid time.
Without one, they are placed at the analysis time — the centre of the
window. Either way, time_offset shifts individual entries relative to that
reference; it must stay within (-freq/2, +freq/2] or perfect_model_obs
will reject the observation as outside the window.
import datetime
# Obs at the analysis time / window centre (default)
entry_centre = ObsNetworkEntry(..., time_offset=datetime.timedelta(0))
# Obs 1 hour after the analysis time
entry_late = ObsNetworkEntry(..., time_offset=datetime.timedelta(hours=1))
MOM6 example#
mom6_perfect_model.py shows a complete ocean example: synthetic temperature
and salinity profiles on a sparse lat/lon grid at six depths, drawn from a
MOM6 history file via MOM6StateProvider with daily assimilation windows
anchored on the model output times.
Parallel execution#
Each window runs in its own subdirectory (pmo_run_dir/windows/{timestamp}/)
with symlinks back to the shared executable and initial-conditions files.
This avoids file conflicts when ProcessPoolExecutor runs multiple windows
simultaneously.
Use max_workers=1 when debugging, or when perfect_model_obs itself
advances the model state (since state advancement must be sequential):
written = generate_obs_sequences(config, source, max_workers=1)