Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Comparing MOM6 output with in-situ ocean observations (small domain, serial)

In this tutorial we will get familiar with the basics of using model2obs to interpolate MOM6 output onto the space of in-situ observations stored in CrocoLake. By the end of it, you will learn to:

  • explore data in CrocoLake

  • interpolate MOM6 output into CrocoLake observations space

  • display the results on an interactive map

  • display the results on an interactive 2D plot

Reminder: we’re looking at the machinery, not really at the science (that is up to you!)

This version uses output from a local CESM-MOM6 run around the Hawaiian islands (case hawaii_pmo, 3 daily snapshots: 2023-06-16, 2023-06-17 and 2023-06-18, on a grid spanning 20.03-24.98 N and 155.03-159.98 W). The CrocoLake observation files were pre-generated and are already in place.

Model data is read from /glade/derecho/scratch/emilanese/m2o_data/hawaii/mom6, and everything model2obs produces is stored under /glade/derecho/scratch/emilanese/m2o_data/hawaii/m2o.

CrocoLake

CrocoLake is a flexible parquet dataset that is fast and easy to access and filter. A copy of the CrocoLake version with physical variables ships with model2obs and is pointed at by $CROCOLAKE_PATH, so let’s set it up:

We now have a look at the CrocoLake data that we are going to work with: we load the observations in the region and time covered by the model output, and plot their positions on a map.

The region boundaries used here (LAT0, LAT1, LON0, LON1) are the same ones that were used to build the obs_seq files: deliberately a bit wider than the model domain, so that some observations fall outside the grid.

Running the workflow

The obs_seq files that model2obs will consume have already been generated from CrocoLake (one file per model snapshot, see data assimilation tutorial for how to build them) and are in the folder declared as obs_seq_in_folder in model2obs.config_tutorial_hawaii.yaml. Their names sort in the same order as the model output files, which is what model2obs relies on when pairing them up. Note: per model2obs design, obs_seq_in_folder must contain only obs_seq files.

Running the workflow to interpolate the model is quite simple: create a WorkflowModelObs instance using a configuration file, and then call its run() method. For this tutorial we will use the configuration file model2obs.config_tutorial_hawaii.yaml. A template file to use as reference is also provided at config_templates/config_template_MOM6.yaml.

While running, model2obs generates temporary input files that tell DART’s perfect_model_obs executable where to find MOM6 and CrocoLake data to perform the interpolation.

If you look through the log, you’ll see a passage that says:

           Convex hull boundaries: 
            Longitude: 200.03° to 204.97° (0-360 convention)
            Latitude: 20.02° to 24.98°
           Number of observations before filtering to convex hull: 4980
           Number of observations after filtering to convex hull: 2968
           Percentage of original observations retained: 59.6%

This is model2obs automatically removing observations that fall outside the MOM6 domain.

Output and bias estimate

model2obs generates a parquet dataset that contains the values of the CrocoLake observations, the MOM6 model data interpolated onto the observations space, and some basic statistics.

The dataframe by default is stored to model_obs_df (as a dask dataframe), and model2obs offers tools to access all data, only the successful interpolations, or only the failed interpolations. For now, let’s load only the interpolations that succeeded (the output message from the previous cell reports how many of the total they are):

With this table, we can already look at the overall bias of our model with respect to the observations. We do so by computing the mean of the difference variable, which is defined as obs - interpolated_model:

For this three-day Hawaii run the temperature bias is about -0.13 degC with an RMSE of 0.42 degC, i.e. the model is slightly warmer than the Argo observations, and the systematic part of the misfit is small compared to its scatter. The salinity numbers are tiny because CrocoLake stores salinity in kg/kg in this dataset, so read them as a relative rather than an absolute offset.

Displaying the interactive map

Loading the interactive map to explore the successful interpolations is as simple as importing the widget and passing the dataframe to it. We also pass a MapConfig to frame the map on the Hawaii region:

Comparing a single profile

The map above mixes observations coming from different platforms, depths and times. To judge how the model does against a single vertical profile, we can isolate the observations that belong to the same profile, i.e. those sharing the same latitude, longitude and time, and then use the interactive profile widget.

Let’s first see how many distinct profiles we have, and pick the one with the most observations:

Interactive profile

InteractiveWidgetProfile plots any pair of columns of the dataframe against each other. Plotting obs (or interpolated_model) versus vertical gives us the vertical profile; the y-axis is inverted by default, so that depth grows downwards.

Use the dropdowns to swap the x-axis between obs, interpolated_model and difference, and the Types selector to switch between temperature and salinity (ctrl+click to select more than one type at a time):

1:1 comparison

Putting the observations on the x-axis and the interpolated model on the y-axis instead gives the classic 1:1 (scatter) comparison: the closer the points fall to the diagonal, the better the model reproduces the observations. Here we also turn off the y-axis inversion, since neither axis is a depth anymore:

Interpolation errors

The dataframe built by model2obs during WorkflowModelObs.run() contains the column interpolated_model_QC, which stores information about the quality of the interpolation. The value is set by DART’s perfect_model_obs program, and you can find more information about it here and here. In general, for this workflow we want QC≤2, and indeed the method get_good_model_obs_df() that we used earlier uses this criterion.

In this Hawaii run, however all interpolations were successful. The cells below are therefore left commented out: they are suggestions to follow whenever your own run reports a non-zero number of failed interpolations. Uncomment and run them in that case.

Had there been failures, the two cells above would have shown:

  1. when an interpolation fails, a value of -888888 is assigned to the interpolated model, so never feed a raw dataframe to your statistics: always filter on the QC first (or use get_good_model_obs_df());

  2. from head(), values of interpolated_model_QC greater than 1000;

The unique values of the QC flag are the quickest way to classify the failures:

Values greater than 1000 are given as 1000 + failure_code, with failure_code from here. The QC flag 1018 indicates that one or more grid points required for the interpolation are not in the basin, so the interpolation cannot be performed: this is the flag you get for observations outside the domain or close to the sea/land border, and it is the one you would cross-check against the map above.

Finally, if you want to load all the data at once (successful and failed interpolations together), you can use the following command: