From 7bfd329fd25f0e42adea7430a78a8ce5703d969e Mon Sep 17 00:00:00 2001
From: lkugler <lukas.kugler@gmail.com>
Date: Wed, 3 May 2023 15:40:48 +0200
Subject: [PATCH] consistent configurations

---
 config/{cfg.py => exp_example.py} |  74 +++++-----
 cycled_exp.py                     |  12 +-
 dartwrf/assim_synth_obs.py        |  70 +---------
 dartwrf/create_obsseq.py          |   2 -
 dartwrf/dart_nml.py               | 216 ++++++++++++++++++++++++++++++
 5 files changed, 268 insertions(+), 106 deletions(-)
 rename config/{cfg.py => exp_example.py} (53%)
 create mode 100644 dartwrf/dart_nml.py

diff --git a/config/cfg.py b/config/exp_example.py
similarity index 53%
rename from config/cfg.py
rename to config/exp_example.py
index 8a833a6..2f158be 100755
--- a/config/cfg.py
+++ b/config/exp_example.py
@@ -1,86 +1,98 @@
 from dartwrf import utils
 
 exp = utils.Experiment()
-exp.expname = "obs-cold_localization-narrow"
+exp.expname = "test_newcode"
 exp.model_dx = 2000
-exp.n_ens = 10
-
-exp.filter_kind = 1
-exp.prior_inflation = 0
-exp.post_inflation = 4
-exp.sec = True
-exp.cov_loc_vert_km_horiz_km = (4, 40)
+exp.n_ens = 40
 exp.superob_km = False  # False or int (spatial averaging of observations)
-exp.adjust_obs_impact = False
 
 exp.use_existing_obsseq = False  # False or pathname (use precomputed obs_seq.out files)
-exp.use_existing_obsseq = '/users/students/lehre/advDA_s2023/dartwrf_tutorial/very_cold_observation.out'
+#exp.use_existing_obsseq = '/users/students/lehre/advDA_s2023/dartwrf_tutorial/very_cold_observation.out'
 
 # path to the nature run, where we take observations from
-exp.nature = '/users/students/lehre/advDA_s2023/data/sample_nature/'
+exp.nature_wrfout = '/jetfs/home/lkugler/data/sim_archive/exp_v1.18_P1_nature/2008-07-30_06:00/1/wrfout_d01_%Y-%m-%d_%H:%M:%S'
 
 exp.input_profile = '/mnt/jetfs/home/lkugler/data/initial_profiles/wrf/ens/2022-03-31/raso.fc.<iens>.wrfprof'
 
+
+exp.dart_nml = {'&assim_tools_nml':
+                    dict(assim_tools_nml='.false.',
+                            filter_kind='1',
+                            sampling_error_correction='.true.',
+                            # obs_impact_filename='/jetfs/home/lkugler/DART-WRF/templates/impactfactor_T.txt',
+                            ),
+                '&filter_nml':
+                    dict(ens_size=str(exp.n_ens),
+                            num_output_state_members=str(exp.n_ens),
+                            num_output_obs_members=str(exp.n_ens),
+                            inf_flavor=['2', '0'],
+                        ),
+                '&location_nml':
+                    dict(horiz_dist_only='.true.',
+                        ),
+                }
+
+
 # n_obs can be 22500: 2km, 5776: 4km, 121: 30km, 256:16x16 (20km); 961: 10km resoltn 
 # if radar: then n_obs is for each observation height level
 
-vis = dict(plotname='VIS 0.6µm', plotunits='[1]',
+vis = dict(var_name='VIS 0.6µm', unit='[1]',
            kind='MSG_4_SEVIRI_BDRF', sat_channel=1, 
            n_obs=961, obs_locations='square_array_evenly_on_grid',
            # n_obs=1, obs_locations=[(44.141, -0.99)],
            error_generate=0.03, error_assimilate=0.03,
-           cov_loc_radius_km=20)
+           loc_horiz_km=20)
 
-wv62 = dict(plotname='Brightness temperature WV 6.2µm', plotunits='[K]',
+wv62 = dict(var_name='Brightness temperature WV 6.2µm', unit='[K]',
             kind='MSG_4_SEVIRI_TB', sat_channel=5, 
             n_obs=961,  obs_locations='square_array_evenly_on_grid',
             error_generate=1., error_assimilate=2., 
-            cov_loc_radius_km=20)
+            loc_horiz_km=20)
 
-wv73 = dict(plotname='Brightness temperature WV 7.3µm', plotunits='[K]',
+wv73 = dict(var_name='Brightness temperature WV 7.3µm', unit='[K]',
             kind='MSG_4_SEVIRI_TB', sat_channel=6, 
             n_obs=961, obs_locations='square_array_evenly_on_grid',
             error_generate=1., error_assimilate=3., 
-            cov_loc_radius_km=20)
+            loc_horiz_km=20)
 
-ir108 = dict(plotname='Brightness temperature IR 10.8µm', plotunits='[K]',
+ir108 = dict(var_name='Brightness temperature IR 10.8µm', unit='[K]',
              kind='MSG_4_SEVIRI_TB', sat_channel=9, 
              n_obs=1, obs_locations='square_array_evenly_on_grid',
              error_generate=5., error_assimilate=10.,
-             cov_loc_radius_km=32)
+             loc_horiz_km=32)
 
-radar = dict(plotname='Radar reflectivity', plotunits='[dBz]',
+radar = dict(var_name='Radar reflectivity', unit='[dBz]',
              kind='RADAR_REFLECTIVITY', 
              n_obs=5776, obs_locations='square_array_evenly_on_grid',
              error_generate=2.5, error_assimilate=2.5,
              heights=range(2000, 14001, 2000),
-             cov_loc_radius_km=1)
+             loc_horiz_km=20, loc_vert_km=2.5)
 
-t = dict(plotname='Temperature', plotunits='[K]',
+t = dict(var_name='Temperature', unit='[K]',
          kind='RADIOSONDE_TEMPERATURE', 
          #n_obs=22500, obs_locations='square_array_evenly_on_grid',
          n_obs=1, obs_locations=[(45., 0.)],
          error_generate=0.2, error_assimilate=0.2,
          heights=[1000,], #range(1000, 17001, 2000),
-         cov_loc_radius_km=50)
+         loc_horiz_km=50, loc_vert_km=2.5)
 
-q = dict(plotname='Specific humidity', plotunits='[kg/kg]',
+q = dict(var_name='Specific humidity', unit='[kg/kg]',
          kind='RADIOSONDE_SPECIFIC_HUMIDITY', n_obs=1,
          error_generate=0., error_assimilate=5*1e-5,
          heights=[1000], #range(1000, 17001, 2000),
-         cov_loc_radius_km=0.1)
+         loc_horiz_km=0.1, loc_vert_km=2.5)
 
-t2m = dict(plotname='SYNOP Temperature', plotunits='[K]',
+t2m = dict(var_name='SYNOP Temperature', unit='[K]',
            kind='SYNOP_TEMPERATURE', n_obs=1, 
            error_generate=0.1, error_assimilate=0.1,
-           cov_loc_radius_km=40)
+           loc_horiz_km=40, loc_vert_km=2.5)
 
-psfc = dict(plotname='SYNOP Pressure', plotunits='[Pa]',
+psfc = dict(var_name='SYNOP Pressure', unit='[Pa]',
             kind='SYNOP_SURFACE_PRESSURE', n_obs=1, 
             error_generate=50., error_assimilate=100.,
-            cov_loc_radius_km=32)
+            loc_horiz_km=32, loc_vert_km=5)
 
-exp.observations = [t2m]
+exp.observations = [vis]
 exp.update_vars = ['U', 'V', 'W', 'THM', 'PH', 'MU', 'QVAPOR', 'QCLOUD', 'QICE', 'PSFC']
-#exp.update_vars = ['U', 'V', 'W', 'T', 'PH', 'MU', 'QVAPOR', 'PSFC']
+
 
diff --git a/cycled_exp.py b/cycled_exp.py
index 243ff26..8e28725 100755
--- a/cycled_exp.py
+++ b/cycled_exp.py
@@ -1,22 +1,20 @@
 #!/usr/bin/python3
 import os, sys, shutil, glob, warnings
 import datetime as dt
-
-from dartwrf.utils import script_to_str
 from dartwrf.workflows import WorkFlows
 
 if __name__ == "__main__":
     """
     Run a cycled OSSE with WRF and DART.
     """
-    w = WorkFlows(exp_config='cfg.py', server_config='jet.py')
+    w = WorkFlows(exp_config='exp_example.py', server_config='jet.py')
 
     timedelta_integrate = dt.timedelta(minutes=15)
     timedelta_btw_assim = dt.timedelta(minutes=15)
 
     id = None
 
-    if True:  # warm bubble
+    if False:  # warm bubble
         prior_path_exp = '/jetfs/home/lkugler/data/sim_archive/exp_v1.19_P3_wbub7_noDA'
 
         init_time = dt.datetime(2008, 7, 30, 12)
@@ -28,13 +26,13 @@ if __name__ == "__main__":
         # id = w.run_ideal(depends_on=id)
         # id = w.wrfinput_insert_wbubble(depends_on=id)    
 
-    if False:  # random
+    if True:  # random
         prior_path_exp = '/jetfs/home/lkugler/data/sim_archive/exp_v1.19_P2_noDA'
 
         init_time = dt.datetime(2008, 7, 30, 12)
         time = dt.datetime(2008, 7, 30, 13)
-        last_assim_time = dt.datetime(2008, 7, 30, 14)
-        forecast_until = dt.datetime(2008, 7, 30, 18)
+        last_assim_time = dt.datetime(2008, 7, 30, 13)
+        forecast_until = dt.datetime(2008, 7, 30, 13, 15)
 
         w.prepare_WRFrundir(init_time)
         # id = w.run_ideal(depends_on=id)
diff --git a/dartwrf/assim_synth_obs.py b/dartwrf/assim_synth_obs.py
index 7fcc052..ddc39b3 100755
--- a/dartwrf/assim_synth_obs.py
+++ b/dartwrf/assim_synth_obs.py
@@ -8,75 +8,13 @@ from dartwrf.obs import error_models as err
 import dartwrf.create_obsseq as osq
 from dartwrf import wrfout_add_geo
 from dartwrf import obsseq
+from dartwrf import dart_nml
 
 from config.cfg import exp
 from config.cluster import cluster
-earth_radius_km = 6370
 wrfout_format = 'wrfout_d01_%Y-%m-%d_%H:%M:%S'
 
 
-def set_DART_nml(just_prior_values=False):
-    def to_radian_horizontal(cov_loc_horiz_km):
-        cov_loc_radian = cov_loc_horiz_km / earth_radius_km
-        return cov_loc_radian
-
-    def to_vertical_normalization(cov_loc_vert_km, cov_loc_horiz_km):
-        vert_norm_rad = earth_radius_km * cov_loc_vert_km / cov_loc_horiz_km * 1000
-        return vert_norm_rad
-
-    list_obstypes = [obscfg["kind"] for obscfg in exp.observations]
-    list_cov_loc_radius_km = [obscfg["cov_loc_radius_km"] for obscfg in exp.observations]
-    list_cov_loc_radian = [str(to_radian_horizontal(a)) for a in list_cov_loc_radius_km]
-
-    if just_prior_values:  # if not compute posterior
-        template = cluster.scriptsdir + "/../templates/input.eval.nml"
-    else:
-        template = cluster.scriptsdir + "/../templates/input.nml"
-    copy(template, cluster.dartrundir + "/input.nml")
-
-    # impact_factor
-    if exp.adjust_obs_impact:
-        copy(cluster.obs_impact_filename, cluster.dartrundir + "/control_impact_runtime.txt")
-
-    # The keys in `options` are placeholders in input.nml which will be replaced.
-    # How? This is defined here
-    options = {
-        "<adjust_obs_impact>": '.true.' if exp.adjust_obs_impact else '.false.',
-        "<filter_kind>": str(int(exp.filter_kind)),
-        "<sampling_error_correction>": '.true.' if exp.sec else '.false.',
-        "<prior_inflation>": str(exp.prior_inflation),
-        "<post_inflation>": str(exp.post_inflation),
-        "<n_ens>": str(int(exp.n_ens)),
-        "<cov_loc_radian>": "0.00000001",  # dummy value, used for types not mentioned below
-        "<list_obstypes>": "'" + "','".join(list_obstypes) + "'",
-        "<list_cutoffs>": ", ".join(list_cov_loc_radian),
-    }
-    
-    # fail if horiz_dist_only == false but observations contain a satellite channel
-    if exp.cov_loc_vert_km_horiz_km != False:
-        for obscfg in exp.observations:
-            if hasattr(obscfg, "sat_channel"):
-                raise ValueError("Selected vertical localization, but observations contain satellite obs -> Not possible.")
-
-    # Note: only one value of vertical localization possible
-    try:
-        cov_loc_vert_km, cov_loc_horiz_km = exp.cov_loc_vert_km_horiz_km
-        vert_norm_hgt = to_vertical_normalization(cov_loc_vert_km, cov_loc_horiz_km)
-        options["<vert_norm_hgt>"] = str(vert_norm_hgt)
-        options["<horiz_dist_only>"] = ".false."
-    except Exception as e:
-        warnings.warn(str(e)+' - not using vertical localization.')
-        options["<horiz_dist_only>"] = ".true."
-        options["<vert_norm_hgt>"] = "99999.0"  # dummy value
-
-    for key, value in options.items():
-        sed_inplace(cluster.dartrundir + "/input.nml", key, value)
-
-    # input.nml for RTTOV
-    rttov_nml = cluster.scriptsdir + "/../templates/obs_def_rttov.VIS.nml"
-    append_file(cluster.dartrundir + "/input.nml", rttov_nml)
-
-
 def link_nature_to_dart_truth(time):
     """Set a symlink from the WRFout file to be used as nature to the run_DART folder
     
@@ -363,7 +301,7 @@ def evaluate(assim_time,
     if not os.path.isfile(cluster.dartrundir+'/obs_seq.out'):
         raise RuntimeError(cluster.dartrundir+'/obs_seq.out does not exist')
 
-    set_DART_nml(just_prior_values=True)
+    dart_nml.write_namelist(just_prior_values=True)
     filter(nproc=6)
 
     # archiving
@@ -531,7 +469,7 @@ def main(time, prior_init_time, prior_valid_time, prior_path_exp):
 
     # remove any existing observation files
     os.system("rm -f input.nml obs_seq.in obs_seq.out obs_seq.out-orig obs_seq.final")  
-    set_DART_nml()
+    dart_nml.write_namelist()
 
     print("prepare nature")
     prepare_nature_dart(time)  # link WRF files to DART directory
@@ -556,7 +494,7 @@ def main(time, prior_init_time, prior_valid_time, prior_path_exp):
         prepare_inflation_2(time, prior_init_time)
 
     print(" 3) run filter ")
-    set_DART_nml()
+    dart_nml.write_namelist()
     filter(nproc=nproc)
     archive_filteroutput(time)
 
diff --git a/dartwrf/create_obsseq.py b/dartwrf/create_obsseq.py
index c0a8a13..12aeab6 100755
--- a/dartwrf/create_obsseq.py
+++ b/dartwrf/create_obsseq.py
@@ -5,10 +5,8 @@ according to which observations are generated and subsequently assimilated.
 import os, sys, warnings
 import numpy as np
 import datetime as dt
-import csv
 from pysolar.solar import get_altitude, get_azimuth
 
-from config.cfg import exp
 from config.cluster import cluster
 from dartwrf.obs import calculate_obs_locations as col
 from dartwrf import utils
diff --git a/dartwrf/dart_nml.py b/dartwrf/dart_nml.py
new file mode 100644
index 0000000..2271506
--- /dev/null
+++ b/dartwrf/dart_nml.py
@@ -0,0 +1,216 @@
+from dartwrf.utils import append_file
+
+from config.cfg import exp
+from config.cluster import cluster
+
+earth_radius_km = 6370
+
+def read_namelist(filepath):
+    """Read the DART namelist file into a dictionary.
+    
+    Args:
+        filepath (str): Path to namelist file
+    
+    Returns:
+        dict: keys are namelist sections, values are dictionaries of namelist variables
+    """
+    
+    d = dict()
+    # read file into a list of strings
+    with open(filepath, 'r') as f:
+        lines = f.readlines()
+
+    for line in lines:
+        # skip whitespace
+        line = line.strip()
+
+        # skip comments
+        if not line.startswith('#'):
+
+            # skip empty lines
+            if len(line) > 0:
+
+                # namelist section
+                if line.startswith('&'):
+                    section = line.lower()
+                    d[section] = dict()
+
+                # namelist variable
+                else:
+                    # split line into variable name and value
+                    var, val = line.split('=')
+                    var = var.strip().lower()
+                    val = val.strip()
+
+                    # split value into list if possible
+                    if ',' in val:
+                        val = val.split(',')
+                        val = [v.strip() for v in val]
+
+                    # add variable to dictionary
+                    d[section][var] = val
+    return d
+
+
+def write_namelist_from_dict(d, filepath):
+    """Write a DART namelist dictionary to a file.
+    
+    Args:
+        d (dict): keys are namelist sections, values are dictionaries of namelist variables
+        filepath (str): Path to namelist file
+    """
+    with open(filepath, 'w') as f:
+        for section in d:
+            f.write(section+'\n')
+            for var in d[section]:
+                val = d[section][var]
+                if isinstance(val, list):
+                    val = ', '.join(val)
+                f.write('\t '+var+' = '+str(val)+'\n')
+            f.write('\t /\n\n')
+
+
+def _get_list_of_localizations():
+    """Compile the list of localizations for the DART namelist variables
+
+    Vertical localization can be defined in section &location_nml of 'input.nml'
+    using following namelist variables:
+        special_vert_normalization_obs_types     (list of str)
+        special_vert_normalization_pressures     (list of float)
+        special_vert_normalization_heights       (list of float)
+        special_vert_normalization_levels        (list of float)
+        special_vert_normalization_scale_heights (list of float)
+
+    To use scale height normalization, set obsdict['loc_vert_scaleheight'] = 0.5
+    To use height normalization, set obsdict['loc_vert_km'] = 3.0
+
+    Args:
+        exp (Experiment): Experiment object
+    
+    Returns:
+        l_obstypes (list of str): entries for `special_vert_normalization_obs_types`
+        l_loc_horiz_rad (list of str): entries for `special_localization_cutoffs`
+        l_loc_vert_km (list of str): entries for `special_vert_normalization_heights`
+        l_loc_vert_scaleheight (list of str): entries for `special_vert_normalization_scale_heights`
+    """
+    def to_radian_horizontal(cov_loc_horiz_km):
+        cov_loc_radian = cov_loc_horiz_km / earth_radius_km
+        return cov_loc_radian
+
+    def to_vertical_normalization(cov_loc_vert_km, cov_loc_horiz_km):
+        vert_norm_rad = earth_radius_km * cov_loc_vert_km / cov_loc_horiz_km * 1000
+        return vert_norm_rad
+
+    l_obstypes = []
+    l_loc_horiz_rad = []
+    l_loc_vert_km = []
+    l_loc_vert_scaleheight = []
+
+    for obscfg in exp.observations:
+
+        l_obstypes.append(obscfg["kind"])
+        loc_horiz_km = obscfg["loc_horiz_km"]
+
+        # compute horizontal localization
+        loc_horiz_rad = str(to_radian_horizontal(loc_horiz_km))
+        l_loc_horiz_rad.append(loc_horiz_rad)
+
+        # compute vertical localization
+
+        # choose either localization by height or by scale height
+        if hasattr(obscfg, "loc_vert_km") and hasattr(obscfg, "loc_vert_scaleheight"):
+            raise ValueError("Observation config contains both loc_vert_km and loc_vert_scaleheight. Please choose one.")
+        
+        elif hasattr(obscfg, "loc_vert_km"):  # localization by height
+            loc_vert_km = str(obscfg["loc_vert_km"])
+
+            vert_norm_hgt = to_vertical_normalization(loc_vert_km, loc_horiz_km)
+            l_loc_vert_km.append(vert_norm_hgt)
+
+        elif hasattr(obscfg, "loc_vert_scaleheight"):  # localization by scale height
+            loc_vert_scaleheight = str(obscfg["loc_vert_scaleheight"])
+
+            # no conversion necessary, take the values as defined in obscfg
+            l_loc_vert_scaleheight.append(loc_vert_scaleheight)
+
+
+    # fail when both localization by height and scale height are requested
+    if len(l_loc_vert_km) > 0 and len(l_loc_vert_scaleheight) > 0:
+        raise ValueError("List of observation configurations contain both height and scale-height localization. Please choose one.")
+    
+    # set the other (unused) list to a dummy value
+    if len(l_loc_vert_km) > 0:
+        l_loc_vert_scaleheight = [-1,]
+    else:
+        l_loc_vert_km = [-1,]
+    
+    return l_obstypes, l_loc_horiz_rad, l_loc_vert_km, l_loc_vert_scaleheight
+
+
+def write_namelist(just_prior_values=False):
+    """Set DART namelist variables in 'input.nml' file.
+    
+    1. Takes the default namelist is the one already defined in the DART source code
+    2. Calculates localization parameters from the observation configurations
+    3. Overwrites other parameters as defined in the experiment configuration
+    4. Writes the namelist to the DART run directory
+
+    Args:
+        just_prior_values (bool, optional): If True, only compute prior values, not posterior. Defaults to False.
+
+    Raises:
+        ValueError: If both height and scale-height localization are requested
+
+    Returns:
+        None
+   """
+    list_obstypes, list_loc_horiz_rad, list_loc_vert_km, list_loc_vert_scaleheight = _get_list_of_localizations()
+
+    nml = read_namelist(cluster.dart_srcdir + "/input.nml")
+
+    # make sure that observations defined in `exp.observations` are assimilated
+    nml['&obs_kind_nml']['assimilate_these_obs_types'] = list_obstypes
+    
+    # dont compute posterior, just evaluate prior
+    if just_prior_values:  
+        nml['&filter_nml']['compute_posterior'] = '.false.'
+        nml['&filter_nml']['output_members'] = '.false.'
+        nml['&filter_nml']['output_mean'] = '.false.'
+        nml['&filter_nml']['output_sd'] = '.false.'
+        nml['&obs_kind_nml']['assimilate_these_obs_types'] = []
+        nml['&obs_kind_nml']['evaluate_these_obs_types'] = list_obstypes
+
+
+    # write localization variables
+    nml['&assim_tools_nml']['special_localization_obs_types'] = list_obstypes
+    nml['&assim_tools_nml']['special_localization_cutoffs'] = list_loc_horiz_rad
+
+    nml['&location_nml']['special_vert_normalization_obs_types'] = list_obstypes
+    nml['&location_nml']['special_vert_normalization_heights'] = list_loc_vert_km
+    nml['&location_nml']['special_vert_normalization_scale_heights'] = list_loc_vert_scaleheight
+
+
+    # overwrite namelist with DART-WRF/config/ configuration
+    for key, value in exp.dart_nml.items():
+
+        # if key is not in namelist, add it
+        if key not in nml:
+            nml[key] = {}
+
+        # overwrite entry in each dictionary
+        nml[key] = value
+
+
+    # final checks
+    # fail if horiz_dist_only == false but observations contain a satellite channel
+    if nml['&location_nml']['horiz_dist_only'] == '.false.':
+        for obscfg in exp.observations:
+            if hasattr(obscfg, "sat_channel"):
+                raise ValueError("Selected vertical localization, but observations contain satellite obs -> Not possible.")
+
+    # write to file
+    write_namelist_from_dict(nml, cluster.dartrundir + "/input.nml")
+
+    # append section for RTTOV
+    rttov_nml = cluster.scriptsdir + "/../templates/obs_def_rttov.VIS.nml"
+    append_file(cluster.dartrundir + "/input.nml", rttov_nml)
\ No newline at end of file
-- 
GitLab