diff --git a/.gitignore b/.gitignore index 6f0f7e6561..e168574f1b 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ rtcoef* advance_time closest_member_tool +cmems_ssh_to_obs cmems_sst_to_obs compare_states compute_error diff --git a/assimilation_code/modules/utilities/read_csv_mod.f90 b/assimilation_code/modules/utilities/read_csv_mod.f90 index eb15e705d6..b9950e0f94 100644 --- a/assimilation_code/modules/utilities/read_csv_mod.f90 +++ b/assimilation_code/modules/utilities/read_csv_mod.f90 @@ -5,8 +5,8 @@ ! Utility routines for reading simple ASCII, CSV-like tabular data. ! ! This module provides a lightweight interface to work with -! non-NetCDF data products. It supports files with a single -! header row and delimited fields (comma, semicolon, or whitespace). +! non-NetCDF data products. It supports files with multiple +! header rows and delimited fields (comma, semicolon, or whitespace). ! ! Some features: ! - Cached CSV file handle with header, delimiter, and row & column count. @@ -61,7 +61,8 @@ module read_csv_mod private character(len=256) :: filename = '' integer :: nrows = 0 - integer :: ncols = 0 + integer :: ncols = 0 + integer :: skip = 0 ! Optional lines to skip before the header integer :: iunit = -1 character :: delim = ',' character(len=512) :: fields(MAX_NUM_FIELDS) @@ -332,7 +333,7 @@ subroutine csv_skip_header(cf, context) type(csv_file_type), intent(inout) :: cf character(len=*), intent(in), optional :: context -integer :: io +integer :: i, io character(len=MAX_FIELDS_LEN) :: line character(len=*), parameter :: routine = 'csv_skip_header' @@ -346,12 +347,15 @@ subroutine csv_skip_header(cf, context) return endif -read(cf%iunit, '(A)', iostat=io) line -if (io /= 0) then - write(string1,'(A,I0)') 'READ(header) failed, io=', io - call error_handler(E_ERR, routine, string1, context) - return -endif +! Start skipping (include any other unnecessary lines) +do i = 1, cf%skip+1 + read(cf%iunit, '(A)', iostat=io) line + if (io /= 0) then + write(string1,'(A,I0)') 'READ(header) failed, io=', io + call error_handler(E_ERR, routine, string1, context) + return + endif +enddo end subroutine csv_skip_header @@ -372,37 +376,60 @@ end function csv_get_nrows ! Open a CSV handle: cache header/dims. ! By doing so, we won't need to open the file ! every time to read header or get dimensions. -subroutine csv_open(fname, cf, forced_delim, context) +subroutine csv_open(fname, cf, skip_lines, forced_delim, context) character(len=*), intent(in) :: fname type(csv_file_type), intent(out) :: cf +integer, intent(in), optional :: skip_lines character(len=*), intent(in), optional :: forced_delim character(len=*), intent(in), optional :: context character(len=*), parameter :: routine = 'csv_open' -integer :: io +integer :: i, io character(len=MAX_FIELDS_LEN) :: line ! Reset cf%filename = fname cf%nrows = 0 cf%ncols = 0 +cf%skip = 0 cf%iunit = -1 cf%delim = ',' cf%fields = '' cf%is_open = .false. -! Number of rows (excluding header) +! Number of rows (excluding 1 header line) cf%nrows = csv_get_nrows_from_file(fname, context) -! Read header and delimiter +! Open the file cf%iunit = open_file(fname, action='read', form='formatted') +! Check if any lines need to be skipped on top of the header +if (present(skip_lines)) then + cf%skip = skip_lines + + do i = 1, cf%skip + read(cf%iunit, '(A)', iostat=io) line + + if (io /= 0) then + call csv_close(cf) + + write(string1, '(A, I0)') 'Got bad read code from input file, io = ', io + call error_handler(E_ERR, routine, string1, context) + return + endif + enddo +endif + +! Update data rows +cf%nrows = cf%nrows - cf%skip + +! The header read(cf%iunit, '(A)', iostat=io) line if (io /= 0) then call csv_close(cf) - + write(string1, '(A, I0)') 'Got bad read code from input file, io = ', io call error_handler(E_ERR, routine, string1, context) return @@ -429,6 +456,7 @@ subroutine csv_close(cf) cf%filename = '' cf%nrows = 0 cf%ncols = 0 +cf%skip = 0 cf%iunit = -1 cf%delim = ',' cf%fields = '' diff --git a/assimilation_code/modules/utilities/read_csv_mod.rst b/assimilation_code/modules/utilities/read_csv_mod.rst index 4b47885eec..a37d4c37c1 100644 --- a/assimilation_code/modules/utilities/read_csv_mod.rst +++ b/assimilation_code/modules/utilities/read_csv_mod.rst @@ -140,6 +140,10 @@ What this module does - values that cannot be converted to integer are returned as ``MISSING_I`` - values that cannot be converted to real are returned as ``MISSING_R8`` +- If a file has extra lines on top (typically including links and data source + information) before the actual header, these can be skipped when opening the + file with ``csv_open`` using the optional argument ``skip_lines`` + - Treats backslash (``\``) as an escape character, preventing interpretation of the following character during parsing. What this module does not do @@ -150,7 +154,6 @@ What this module does not do - It does not support: - - multiple header lines - comment lines or metadata blocks - embedded newlines inside quoted fields diff --git a/guide/available-observation-converters.rst b/guide/available-observation-converters.rst index a074b8d6e5..26a86c09b2 100644 --- a/guide/available-observation-converters.rst +++ b/guide/available-observation-converters.rst @@ -21,6 +21,7 @@ Each directory has at least one converter: - ``BATS``: :ref:`bats` - ``CHAMP``: :ref:`champ` - ``cice``: :ref:`cice_to_obs` +- ``cmems_ssh_l3``: :ref:`cmems_ssh_to_obs` - ``cmems_sst_l3s``: :ref:`cmems_sst_to_obs` - ``CNOFS``: See ``DART/observations/obs_converters/CNOFS`` - ``CONAGUA``: :ref:`conagua` diff --git a/index.rst b/index.rst index 4cac87f4ee..49676a872f 100644 --- a/index.rst +++ b/index.rst @@ -335,6 +335,7 @@ References observations/obs_converters/CHAMP/README observations/obs_converters/BATS/readme observations/obs_converters/cice/cice_to_obs + observations/obs_converters/cmems_ssh_l3/readme observations/obs_converters/cmems_sst_l3s/readme observations/obs_converters/CONAGUA/README observations/obs_converters/COSMOS/COSMOS_to_obs diff --git a/models/ROMS_rutgers/preprocess_ocean_obs.py b/models/ROMS_rutgers/preprocess_ocean_obs.py new file mode 100644 index 0000000000..eb9bfab19e --- /dev/null +++ b/models/ROMS_rutgers/preprocess_ocean_obs.py @@ -0,0 +1,212 @@ +""" +preprocess_ocean_obs.py -- Filter an obs_seq file by bathymetric depth and + optionally replace observation error with a depth- + dependent model. + +When --obs-type is given, only that type is depth-filtered and error-updated; +all other types are kept unchanged. Without --obs-type, all obs are processed. + +Usage examples +-------------- +# All obs, depth filter + depth-dependent error (defaults): +python preprocess_ocean_obs.py obs_seq.all obs_seq.all_trim \ + --roms-file roms_restart.nc + +# All obs, depth filter only (keep original error variance): +python preprocess_ocean_obs.py obs_seq.all obs_seq.all_trim \ + --roms-file roms_restart.nc --no-depth-error + +# One obs type, depth filter + error update, custom depth cut-off +# (other types in the file are kept unchanged): +python preprocess_ocean_obs.py obs_seq.all obs_seq.deep \ + --roms-file roms_restart.nc \ + --obs-type SATELLITE_SSH \ + --min-depth 500 + +# All obs, error update only (no depth filtering): +python preprocess_ocean_obs.py obs_seq.all obs_seq.err_updated \ + --roms-file roms_restart.nc --no-depth-filter + +""" + +# Module imports +from scipy.spatial import cKDTree + +import argparse +import sys +import pandas as pd +import numpy as np +import xarray as xr +import pydartdiags.obs_sequence.obs_sequence as obsq + + +# CLI +def parse_args(): + p = argparse.ArgumentParser( + description="Trim an obs_seq file by ROMS bathymetric depth.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + # Required positional arguments + p.add_argument("obs_in", help="Input obs_seq file") + p.add_argument("obs_out", help="Output obs_seq file") + p.add_argument("--roms-file", required=True, + help="ROMS NetCDF file with h, mask_rho, lon_rho, lat_rho") + p.add_argument("--obs-type", default=None, + help="Process only this obs type; all others pass through unchanged") + + # Depth filter + filt = p.add_argument_group("depth filter") + filt.add_argument("--no-depth-filter", dest="use_depth_filter", + action="store_false", default=True, + help="Keep obs at all depths") + filt.add_argument("--min-depth", type=float, default=200.0, + help="Remove obs shallower than this [m]") + + # Depth-dependent error + err = p.add_argument_group("depth-dependent error") + err.add_argument("--no-depth-error", dest="use_depth_error", + action="store_false", default=True, + help="Keep original obs error variance") + err.add_argument("--sigma-min", type=float, default=0.04, + help="Deep-ocean obs error std-dev [obs units]") + err.add_argument("--sigma-max", type=float, default=0.08, + help="Shallow-water obs error std-dev [obs units]") + err.add_argument("--h0", type=float, default=500.0, + help="Depth scale for error transition [m]") + + args = p.parse_args() + + if not args.use_depth_filter and not args.use_depth_error: + p.error("--no-depth-filter and --no-depth-error together leave nothing to do.") + + return args + + +# Depth lookup: vectorised via KDTree +def lookup_depths(lon_obs, lat_obs, glon, glat, bath, mask): + """ + Return the bathymetric depth at the nearest wet grid point for every + observation. This is an approximate depth estimate because observations + are not necessarily collocated with the model grid. + """ + wet = mask > 0.5 # 0: land, 1: wet + tree = cKDTree(np.column_stack([glon[wet], glat[wet]])) + _, idx = tree.query(np.column_stack([lon_obs, lat_obs])) + return bath[wet][idx] + + +# Depth-dependent error model +def depth_dependent_error_std(depth, sigma_min, sigma_max, h0): + """Exponential depth-to-error mapping; returns std-dev (not variance).""" + return sigma_min + (sigma_max - sigma_min) * np.exp(-depth / h0) + + +# Main +def main(): + args = parse_args() + + # Read the input obs_seq and ROMS grid + print(f"\nReading obs_seq: {args.obs_in}") + obs_seq = obsq.ObsSequence(args.obs_in) + + print(f"Reading ROMS grid: {args.roms_file}") + with xr.open_dataset(args.roms_file) as roms: + bath = roms["h"].values + mask = roms["mask_rho"].values + glon = np.mod(roms["lon_rho"].values, 360.0) # DART-style + glat = roms["lat_rho"].values + + # Which observation type(s) to process? + df = obs_seq.df.copy() + + # First, find out what obs are available in file: + available = sorted(df["type"].unique()) + print(f"\nObs types in file: {available}") + + if args.obs_type: + # User selected obs type + if args.obs_type not in available: + print(f"ERROR: '{args.obs_type}' not found in file.\n" + f"Available: {available}") + sys.exit(1) + obs_df = df[df["type"] == args.obs_type].copy() + obs_rest = df[df["type"] != args.obs_type].copy() + label = args.obs_type + else: + obs_df = df.copy() + obs_rest = None + label = "all" + + Nobs_in = len(obs_df) + print(f"\nInput obs ({label}): {Nobs_in}") + + if Nobs_in == 0: # empty? + print("No observations to process — exiting.") + sys.exit(0) + + # Figure out the options for depth filtering and + # the error update. + if args.use_depth_filter or args.use_depth_error: + # Depth lookup needed for filter and/or error update + + print("Looking up bathymetric depths ...") + lon_obs = obs_df["longitude"].values + lat_obs = obs_df["latitude"].values + depth = lookup_depths(lon_obs, lat_obs, glon, glat, bath, mask) + else: + depth = None + + if args.use_depth_filter and depth is not None: + keep = np.isfinite(depth) & (depth > args.min_depth) + obs_kept = obs_df.loc[keep].copy() + depth_kept = depth[keep] + else: + obs_kept = obs_df.copy() + depth_kept = depth + + if args.use_depth_error and depth_kept is not None: + sigma = depth_dependent_error_std( + depth_kept, args.sigma_min, args.sigma_max, args.h0) + obs_kept["obs_err_var"] = sigma ** 2 + + # Merge back and re-number: + if obs_rest is not None: + obs_final = pd.concat([obs_rest, obs_kept], ignore_index=True) + obs_final = obs_final.sort_values(["days", "seconds", "obs_num"]). \ + reset_index(drop=True) + else: + obs_final = obs_kept.reset_index(drop=True) + + obs_final["obs_num"] = np.arange(1, len(obs_final) + 1) + + # Write the output obs_seq + obs_seq.df = obs_final + + obs_seq.update_attributes_from_df() + obs_seq._update_linked_list(obs_final) # private, no public API for this? + obs_seq.write_obs_seq(args.obs_out) + + # Print summary + print(f"\nSummary ({label})") + + if args.use_depth_filter: + print(f" Kept: {len(obs_kept)} (depth > {args.min_depth:.0f} m)") + print(f" Removed: {Nobs_in - len(obs_kept)}") + if len(obs_kept): + print(f" Depth range kept: {depth_kept.min():.1f} – {depth_kept.max():.1f} m") + else: + print(f" Depth filter: off | all {Nobs_in} obs kept") + + if args.use_depth_error: + print(f" Error model: sigma = {args.sigma_min} + " + f"({args.sigma_max} - {args.sigma_min}) * exp(-h / {args.h0})") + else: + print(f" Error model: Original obs error variance kept") + + if obs_rest is not None: + print(f"\n Other obs types kept unchanged: {len(obs_rest)}") + + print(f"\n Wrote: {args.obs_out}") + +if __name__ == "__main__": + main() diff --git a/observations/obs_converters/cmems_ssh_l3/cmems_ssh_to_obs.f90 b/observations/obs_converters/cmems_ssh_l3/cmems_ssh_to_obs.f90 new file mode 100644 index 0000000000..466a27bab0 --- /dev/null +++ b/observations/obs_converters/cmems_ssh_l3/cmems_ssh_to_obs.f90 @@ -0,0 +1,323 @@ +! DART software - Copyright UCAR. This open source software is provided +! by UCAR, "as is", without charge, subject to all terms of use at +! http://www.image.ucar.edu/DAReS/DART/DART_download + +! Copernicus SSH converter. The data is based on the +! "Global Ocean Along Track L3 Sea Surface Heights NRT" +! available at: https://data.marine.copernicus.eu/product/SEALEVEL_GLO_PHY_L3_NRT_008_044/description + +! The converter reads in the raw csv file and grabs the sla_filtered +! which is the low-pass filtered sea level anomaly, with DAC, ocean tide, +! internal tide, and long-wavelength-error corrections applied. +! This obs should be smoother, less noisy, and closer to the modeled +! mesoscale signal than its "sla_unfiltered" counterpart. +! The observation error standard deviation is an input namelist option. + +! SSH data is in meters. + + +program cmems_ssh_to_obs + +use types_mod, only : r8, MISSING_I, MISSING_R8 +use time_manager_mod, only : time_type, set_calendar_type, GREGORIAN, get_time, & + set_date, print_date, operator(+), operator(-) +use utilities_mod, only : initialize_utilities, find_namelist_in_file, & + nmlfileunit, error_handler, do_nml_term, E_ERR, & + finalize_utilities, do_nml_file, get_next_filename, & + find_textfile_dims, file_exist, E_MSG, to_upper, & + check_namelist_read +use location_mod, only : VERTISSURFACE +use obs_sequence_mod, only : obs_type, obs_sequence_type, init_obs, get_num_obs, & + static_init_obs_sequence, init_obs_sequence, & + set_copy_meta_data, set_qc_meta_data, write_obs_seq, & + destroy_obs_sequence, destroy_obs +use obs_utilities_mod, only : create_3d_obs, add_obs_to_seq +use obs_kind_mod, only : SATELLITE_SSH +use read_csv_mod, only : csv_file_type, csv_get_nrows, csv_get_field, & + csv_open, csv_close, csv_print_header + +implicit none + +character(len=*), parameter :: source = 'cmems_ssh_to_obs' + +character(len=*), parameter :: SSH_OBS = 'SLA_FILTERED' +integer, parameter :: INFORMATION_LINES = 5 + +character(len=512) :: string1 + +! File variables +character(len=256) :: next_infile +integer :: io, iunit, filenum +integer :: num_new_obs, nfiles +integer :: num_valid_obs +logical :: first_obs = .true. + +! Obs sequence +type(obs_sequence_type) :: obs_seq +type(obs_type) :: obs, prev_obs +type(time_type) :: prev_time +integer :: num_copies = 1, & ! number of copies in sequence + num_qc = 1 ! number of QC entries + +! Data arrays +real(r8), allocatable :: lon(:), lat(:) ! Location +real(r8), allocatable :: val(:) ! Obs value +integer, allocatable :: vqc(:) ! Obs QC + +character(len=512), allocatable :: dat(:), par(:) + +! SSH data structure +type altimetry_obs + type(time_type) :: dat + real(r8) :: lat + real(r8) :: lon + real(r8) :: obs + real(r8) :: err + real(r8) :: oqc +end type altimetry_obs + +type(altimetry_obs), allocatable :: sat(:) + +! csv obs file +type(csv_file_type) :: cf + +!------------------------------------------------------------------------ +! Declare namelist parameters +character(len=256) :: file_list = '' ! List of incoming obs csv files +character(len=256) :: file_out = 'obs_seq.ssh' ! Resulting obs seq file +integer :: avg_obs_per_file = 500000 ! Average number of obs in file +real(r8) :: obs_error_sd = 0.04_r8 ! Obs err sd [m]; often 3 0) then + if (debug) write(*, '(/, A, i0, A)') '> Ready to write ', obs_num, ' observations:' + + call write_obs_seq(obs_seq, file_out) + call destroy_obs(obs) + call destroy_obs_sequence(obs_seq) +else + string1 = 'No obs were converted.' + call error_handler(E_MSG, source, string1) +endif + +call error_handler(E_MSG, source, 'Finished successfully.') + +end subroutine finish_obs + + +!------------------------------------------------------------ +! Clear up memory +subroutine cleanup() + +deallocate(lon, lat, val, vqc) +deallocate(dat, par, sat) + +end subroutine cleanup + +end program cmems_ssh_to_obs diff --git a/observations/obs_converters/cmems_ssh_l3/readme.rst b/observations/obs_converters/cmems_ssh_l3/readme.rst new file mode 100644 index 0000000000..8ffa7c5438 --- /dev/null +++ b/observations/obs_converters/cmems_ssh_l3/readme.rst @@ -0,0 +1,205 @@ +.. _cmems_ssh_to_obs: + +=============================== +CMEMS SSH Along-Track Converter +=============================== + +.. contents:: + :depth: 3 + :local: + +Overview +-------- +This converter ingests **Copernicus Marine Environment Monitoring Service (CMEMS)** +along-track sea surface height (SSH) observations and converts them into a DART +``obs_seq`` file. + +The supported product is: + +- *Global Ocean Along Track L3 Sea Surface Heights NRT* + (Product ID: ``SEALEVEL_GLO_PHY_L3_NRT_008_044``) + +The converter reads CSV files exported from CMEMS and extracts **filtered sea level +anomaly (SLA)** observations for assimilation into ocean models such as ROMS. + +The resulting observations are assigned the DART type: ``SATELLITE_SSH``. The +observations in the sequence file are in units of meters. + +Data Description +---------------- +The converter uses the following variable: ``SLA_FILTERED`` + +This variable represents **low-pass filtered sea level anomaly**, including corrections for +dynamic atmospheric correction (DAC), ocean tides, internal tides, and long-wavelength errors. +Compared to ``SLA_UNFILTERED``, this field is smoother, less noisy, and more +representative of mesoscale variability. This makes it more appropriate for +comparison with model sea surface height (e.g., ROMS ``zeta``). + +Observation Error +----------------- +The converter assigns a uniform observation error standard deviation to all SSH +observations. The error value is specified by the user through the converter +namelist and is written to the output ``obs_seq`` file as observation error +variance. + +Optional Post-Processing +^^^^^^^^^^^^^^^^^^^^^^^^ +Additional filtering and depth-dependent error adjustment can be performed +after the converter is run using the utility script: + +:: + + models/ROMS_Rutgers/preprocess_ocean_obs.py + +This script is intended for model-specific preprocessing of ocean observations +prior to assimilation. It can: + +- remove observations located in shallow-water regions, +- apply depth-dependent observation error models, +- process only selected observation types, +- preserve all other observation types unchanged. + +The script currently uses ROMS bathymetry and land masks from a ROMS restart +or history file. + +For SSH observations, a commonly used workflow is to remove observations in +water shallower than 200 m and increase the observation error in coastal +regions where representativeness errors and unresolved processes are larger. + +The depth-dependent error model is: + +:: + + sigma = sigma_min + (sigma_max - sigma_min) * exp(-h / h0) + +where: + +- ``sigma_min``: deep-ocean observation error standard deviation, +- ``sigma_max``: shallow-water observation error standard deviation, +- ``h``: local bathymetric depth (m), +- ``h0``: transition depth scale (m). + +Example usage: + +:: + + python preprocess_ocean_obs.py obs_seq.all obs_seq.trim \ + --roms-file roms_hist.nc \ + --obs-type SATELLITE_SSH + +Typical output: + +:: + + Reading obs_seq: obs_seq.all + Reading ROMS grid: roms_hist.nc + + Obs types in file: ['BOTTLE_TEMPERATURE', 'CTD_TEMPERATURE', + 'FLOAT_TEMPERATURE', 'MOORING_TEMPERATURE', + 'SATELLITE_SSH', 'XBT_TEMPERATURE'] + + Input obs (SATELLITE_SSH): 3042 + Looking up bathymetric depths ... + + Summary (SATELLITE_SSH) + Kept: 2305 (depth > 200 m) + Removed: 737 + Depth range kept: 205.0 – 5000.0 m + Error model: sigma = 0.04 + (0.08 - 0.04) * exp(-h / 500.0) + + Other obs types kept unchanged: 571 + + Wrote: obs_seq.trim + + +Time Handling +------------- +Observation times are read from ISO-formatted strings, for example: + +:: + + 2026-04-16T08:43:54.000Z + +These are converted to DART ``time_type`` using the Gregorian calendar. + +Usage +----- +1. Prepare a list of input CSV files and place them in a text file, for instance ``ssh_list.txt`` + +2. Configure the namelist in ``input.nml``: + +:: + + &cmems_ssh_to_obs_nml + file_list = 'ssh_file_list.txt' + file_out = 'obs_seq.ssh' + avg_obs_per_file = 500000 + obs_error_sd = 0.04 + debug = .true. + / + ++----------------------+-----------------------+---------------------------+--------------------------------------------------------------+ +| Namelist item | Type | Default Values | Description | ++======================+=======================+===========================+==============================================================+ +| ``file_list`` | character(len=256) | ``''`` (empty string) | Path to a text file containing a list of input CSV files, | +| | | | one filename per line. | ++----------------------+-----------------------+---------------------------+--------------------------------------------------------------+ +| ``file_out`` | character(len=256) | ``'obs_seq.ssh'`` | Name of the DART observation sequence output file. If the | +| | | | file already exists, it will be replaced. | ++----------------------+-----------------------+---------------------------+--------------------------------------------------------------+ +| ``avg_obs_per_file`` | integer | ``500000`` | Estimated average number of observations per input file. | +| | | | Used to pre-allocate sequence memory efficiently. | ++----------------------+-----------------------+---------------------------+--------------------------------------------------------------+ +| ``obs_error_sd`` | real(r8) | ``0.04`` | Uniform observation error standard deviation (meters) | +| | | | assigned to all SSH observations. | ++----------------------+-----------------------+---------------------------+--------------------------------------------------------------+ +| ``debug`` | logical | ``.true.`` | If true, prints diagnostic output including sample | +| | | | observations and processing information. | ++----------------------+-----------------------+---------------------------+--------------------------------------------------------------+ + +1. Run the converter: ``./cmems_ssh_to_obs`` + +2. Output: The resulting observation sequence file is stored in ``obs_seq.ssh`` + +When ``debug`` is turned on, the progress of the converter can be monitored +which would look similar to the following: + +:: + + Input file: #2 ../data/obs_ssh_2.csv + + CSV Header Fields (10 columns): + 1 parameter + 2 platformId + 3 platformType + 4 time + 5 longitude + 6 latitude + 7 depth + 8 pressure + 9 value + 10 valueQc + + * obs # 1, lat: -14.2924, lon: 127.8070, SSH: 0.2940, QC: 0, date: 2026 Apr 14 19:26:36 + * obs # 2, lat: -14.2351, lon: 127.8010, SSH: 0.2820, QC: 0, date: 2026 Apr 14 19:26:37 + * obs # 3, lat: -14.1778, lon: 127.7949, SSH: 0.2690, QC: 0, date: 2026 Apr 14 19:26:38 + * obs # 4, lat: -14.1206, lon: 127.7888, SSH: 0.2560, QC: 0, date: 2026 Apr 14 19:26:39 + * obs # 5, lat: -14.0633, lon: 127.7827, SSH: 0.2420, QC: 0, date: 2026 Apr 14 19:26:40 + * obs # 6, lat: -14.0060, lon: 127.7766, SSH: 0.2280, QC: 0, date: 2026 Apr 14 19:26:41 + * obs # 7, lat: -13.9487, lon: 127.7705, SSH: 0.2130, QC: 0, date: 2026 Apr 14 19:26:42 + * obs # 8, lat: -13.8915, lon: 127.7645, SSH: 0.2000, QC: 0, date: 2026 Apr 14 19:26:43 + * obs # 9, lat: -13.8342, lon: 127.7584, SSH: 0.1910, QC: 0, date: 2026 Apr 14 19:26:43 + * obs # 10, lat: -13.7769, lon: 127.7523, SSH: 0.1850, QC: 0, date: 2026 Apr 14 19:26:44 + + > Ready to write 3042 observations: + write_obs_seq opening formatted observation sequence file "obs_seq.ssh" + write_obs_seq closed observation sequence file "obs_seq.ssh" + cmems_ssh_to_obs Finished successfully. + +References +---------- +- Copernicus Marine Service: + https://marine.copernicus.eu + +- Product documentation: + https://data.marine.copernicus.eu/product/SEALEVEL_GLO_PHY_L3_NRT_008_044 diff --git a/observations/obs_converters/cmems_ssh_l3/work/input.nml b/observations/obs_converters/cmems_ssh_l3/work/input.nml new file mode 100644 index 0000000000..5a905834ea --- /dev/null +++ b/observations/obs_converters/cmems_ssh_l3/work/input.nml @@ -0,0 +1,47 @@ +&preprocess_nml + overwrite_output = .true. + input_obs_qty_mod_file = '../../../../assimilation_code/modules/observations/DEFAULT_obs_kind_mod.F90' + output_obs_qty_mod_file = '../../../../assimilation_code/modules/observations/obs_kind_mod.f90' + input_obs_def_mod_file = '../../../forward_operators/DEFAULT_obs_def_mod.F90' + output_obs_def_mod_file = '../../../forward_operators/obs_def_mod.f90' + obs_type_files = '../../../forward_operators/obs_def_ocean_mod.f90' + quantity_files = '../../../../assimilation_code/modules/observations/ocean_quantities_mod.f90' + / + + +&cmems_ssh_to_obs_nml + file_list = 'ssh_file_list.txt' + file_out = 'obs_seq.ssh' + obs_error_sd = 0.04 + debug = .true. + / + +&obs_def_ocean_nml + max_radial_vel_obs = 10000000 + debug = .false. + / + +&utilities_nml + module_details = .false. + / + +&obs_kind_nml + / + +&location_nml + / + +&obs_sequence_nml + write_binary_obs_sequence = .false. + / + +&obs_sequence_tool_nml + filename_seq = '' + filename_out = '' + print_only = .false. + gregorian_cal = .true. + first_obs_days = -1 + first_obs_seconds = -1 + last_obs_days = -1 + last_obs_seconds = -1 + / diff --git a/observations/obs_converters/cmems_ssh_l3/work/quickbuild.sh b/observations/obs_converters/cmems_ssh_l3/work/quickbuild.sh new file mode 100755 index 0000000000..d10c5fe50e --- /dev/null +++ b/observations/obs_converters/cmems_ssh_l3/work/quickbuild.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# DART software - Copyright UCAR. This open source software is provided +# by UCAR, "as is", without charge, subject to all terms of use at +# http://www.image.ucar.edu/DAReS/DART/DART_download + +main() { + +export DART=$(git rev-parse --show-toplevel) +source "$DART"/build_templates/buildconvfunctions.sh + +CONVERTER=cmems_ssh_l3 +LOCATION=threed_sphere + + +programs=( +advance_time +cmems_ssh_to_obs +obs_seq_to_netcdf +obs_sequence_tool +) + +# build arguments +arguments "$@" + +# clean the directory +\rm -f -- *.o *.mod Makefile .cppdefs + +# build and run preprocess before making any other DART executables +buildpreprocess + +# build +buildconv + + +# clean up +\rm -f -- *.o *.mod + +} + +main "$@"