atmospheric_lidar/scripts/licel2scc.py

Thu, 22 Mar 2018 13:53:18 +0200

author
Iannis B <ioannis@inoe.ro>
date
Thu, 22 Mar 2018 13:53:18 +0200
changeset 142
b1cac5351db6
parent 128
168da625489b
child 159
c31f5388b482
permissions
-rw-r--r--

Added missing variable First_Signal_Rangebin in the supported variables for licel2scc convertion.

ioannis@48 1 """ Command line tool to convert Licel binary files to SCC NetCDF format.
ioannis@48 2 """
ioannis@55 3 import argparse
ioannis@55 4 import glob
ioannis@55 5 import importlib
ioannis@55 6 import logging
ioannis@48 7 import os
ioannis@48 8 import sys
ioannis@55 9
i@128 10 from matplotlib import pyplot as plt
i@128 11 import yaml
i@128 12
ioannis@48 13 from ..licel import LicelLidarMeasurement
ulalume3@67 14 from ..__init__ import __version__
ioannis@48 15
i@103 16 logger = logging.getLogger(__name__)
i@103 17
ioannis@48 18
i@101 19 def create_custom_class(custom_netcdf_parameter_path, use_id_as_name=False, temperature=25., pressure=1020.,
i@101 20 licel_timezone='UTC'):
ioannis@48 21 """ This funtion creates a custom LicelLidarMeasurement subclass,
ioannis@48 22 based on the input provided by the users.
ioannis@48 23
ioannis@48 24 Parameters
ioannis@48 25 ----------
i@101 26 custom_netcdf_parameter_path : str
ioannis@48 27 The path to the custom channels parameters.
i@101 28 use_id_as_name : bool
ioannis@48 29 Defines if channels names are descriptive or transient digitizer IDs.
i@101 30 temperature : float
ioannis@48 31 The ground temperature in degrees C (default 25.0).
i@101 32 pressure : float
ioannis@48 33 The ground pressure in hPa (default: 1020.0).
i@101 34 licel_timezone : str
i@101 35 String describing the timezone according to the tz database.
ioannis@48 36
ioannis@48 37 Returns
ioannis@48 38 -------
ioannis@48 39 CustomLidarMeasurement:
ioannis@48 40 A custom sub-class of LicelLidarMeasurement
ioannis@48 41 """
i@103 42 logger.debug('Reading parameter files: %s' % custom_netcdf_parameter_path)
ioannis@48 43 custom_netcdf_parameters = read_settings_file(custom_netcdf_parameter_path)
ioannis@48 44
ioannis@48 45 class CustomLidarMeasurement(LicelLidarMeasurement):
ioannis@48 46 extra_netcdf_parameters = custom_netcdf_parameters
ioannis@48 47
ulalume3@92 48 def __init__(self, file_list=None):
i@101 49 super(CustomLidarMeasurement, self).__init__(file_list, use_id_as_name, licel_timezone=licel_timezone)
ioannis@48 50
ulalume3@92 51 def set_PT(self):
ioannis@48 52 ''' Sets the pressure and temperature at station level. This is used if molecular_calc parameter is
ioannis@48 53 set to 0 (i.e. use US Standard atmosphere).
ioannis@48 54
ioannis@48 55 The results are stored in the info dictionary.
ioannis@48 56 '''
ioannis@48 57
ioannis@48 58 self.info['Temperature'] = temperature
ioannis@48 59 self.info['Pressure'] = pressure
ioannis@48 60
ioannis@48 61 return CustomLidarMeasurement
ioannis@48 62
ioannis@48 63
ioannis@48 64 def read_settings_file(settings_path):
ioannis@48 65 """ Read the settings file.
ioannis@48 66
ioannis@48 67 The file should contain python code."""
ioannis@48 68 if not os.path.isfile(settings_path):
ioannis@58 69 logging.error("The provided settings path does not correspond to a file.")
ioannis@58 70 sys.exit(1)
ioannis@48 71
ioannis@48 72 dirname, basename = os.path.split(settings_path)
ioannis@48 73 sys.path.append(dirname)
ioannis@48 74
ioannis@48 75 module_name, _ = os.path.splitext(basename)
ioannis@48 76 settings = importlib.import_module(module_name)
ioannis@48 77 return settings
ioannis@48 78
ioannis@48 79
i@128 80 def read_cloudmask_settings_file(settings_file_path):
i@128 81 """ Read the configuration file.
i@128 82
i@128 83 The file should be in YAML syntax."""
i@128 84
i@128 85 if not os.path.isfile(settings_file_path):
i@128 86 logging.error("Wrong path for cloudmask settings file (%s)" % settings_file_path)
i@128 87 sys.exit(1)
i@128 88
i@128 89 with open(settings_file_path) as yaml_file:
i@128 90 try:
i@128 91 settings = yaml.load(yaml_file)
i@128 92 logging.debug("Read cloudmask settings file(%s)" % settings_file_path)
i@128 93 except:
i@128 94 logging.error("Could not parse YAML file (%s)" % settings_file_path)
i@128 95 sys.exit(1)
i@128 96
i@128 97 return settings
i@128 98
i@128 99
ioannis@142 100 def get_cloud_free_files(LidarMeasurementClass, files, settings):
ioannis@142 101 """ Find cloud free periods in the given files.
ioannis@142 102
ioannis@142 103 Depending on the provided settings, it could create plots of cloud mask and
ioannis@142 104 selected cloud-free periods.
ioannis@142 105
ioannis@142 106 Parameters
ioannis@142 107 ----------
ioannis@142 108 LidarMeasurementClass : class
ioannis@142 109 Class used to read the files.
ioannis@142 110 files : list
ioannis@142 111 A list of raw licel file paths.
ioannis@142 112 settings : dict
ioannis@142 113 A dictionary of cloud masking settings.
ioannis@142 114
ioannis@142 115 Returns
ioannis@142 116 -------
ioannis@142 117 file_list : list of lists
ioannis@142 118 A list of lists containing paths to cloud-free files.
ioannis@142 119 """
i@124 120 logger.warning("Starting cloud mask procedure. This is an experimental feature.")
i@124 121
i@124 122 try:
i@124 123 from cloudmask import cloudmask # Import here until we setup a proper installation procedure
i@124 124 except ImportError:
i@124 125 logger.error("Cloud mask module could not be loaded. Please install manually.")
i@124 126 sys.exit(1)
i@124 127
ioannis@142 128 measurement = LidarMeasurementClass(files)
i@128 129 channel = measurement.channels[settings['channel']]
i@124 130 cloud_mask = cloudmask.CloudMaskRaw(channel)
i@128 131
i@128 132 idxs = cloud_mask.cloud_free_periods(settings['cloudfree_period_min'],
i@128 133 settings['file_duration_max'],
i@128 134 settings['max_cloud_height'])
i@128 135
i@128 136 logger.debug('Cloud free indices: {0}'.format(idxs))
i@124 137
i@124 138 if len(idxs) == 0: # If no cloud-free period found
i@124 139 logger.info('No cloud free period found. Nothing converted.')
i@124 140 sys.exit(1)
i@124 141
i@124 142 logger.info("{0} cloud free period(s) found.".format(len(idxs)))
i@128 143
i@128 144 if settings['plot']:
ioannis@142 145 # Plot cloud free periods
ioannis@142 146 cloudfree_filename = "cloudfree_{0}_{1}_{2}.png".format(channel.wavelength,
i@128 147 channel.start_time.strftime('%Y%m%d_%H%M%S'),
i@128 148 channel.stop_time.strftime('%Y%m%d_%H%M%S'))
ioannis@142 149 cloudfree_path = os.path.join(settings['plot_directory'], cloudfree_filename)
i@128 150 fig, _ = cloud_mask.plot_cloudfree(idxs)
i@128 151
ioannis@142 152 plt.savefig(cloudfree_path)
ioannis@142 153 plt.close()
ioannis@142 154
ioannis@142 155 # Plot cloud mask
ioannis@142 156 cloudmask_filename = "cloudmask_{0}_{1}_{2}.png".format(channel.wavelength,
ioannis@142 157 channel.start_time.strftime('%Y%m%d_%H%M%S'),
ioannis@142 158 channel.stop_time.strftime('%Y%m%d_%H%M%S'))
ioannis@142 159 cloudmask_path = os.path.join(settings['plot_directory'], cloudmask_filename)
ioannis@142 160
ioannis@142 161 fig, _ = cloud_mask.plot_mask()
ioannis@142 162
ioannis@142 163 plt.savefig(cloudmask_path)
ioannis@142 164 plt.close()
i@128 165
i@124 166 file_list = []
i@124 167 for idx_min, idx_max in idxs:
i@124 168 current_files = measurement.files[idx_min:idx_max]
i@124 169 file_list.append(current_files)
i@124 170
i@124 171 return file_list
i@124 172
i@124 173
i@124 174 def get_corrected_measurement_id(args, n):
i@124 175 """ Correct the provided measurement id, in case of multiple cloud-free periods. """
i@124 176 if args.measurement_id is not None:
i@126 177 order = int(args.measurement_id[-2:])
i@124 178 new_no = order + n
i@124 179 measurement_id = args.measurement_id[:-2] + str(new_no)
i@124 180 measurement_no = args.measurement_number # The same
i@124 181 else:
i@126 182 measurement_no = str(int(args.measurement_number) + n).zfill(2)
i@124 183 measurement_id = None
i@124 184
i@124 185 return measurement_id, measurement_no
i@124 186
i@124 187
i@124 188 def convert_to_scc(CustomLidarMeasurement, files, dark_pattern, measurement_id, measurement_number):
i@124 189 """ Convert files to SCC. """
i@124 190 measurement = CustomLidarMeasurement(files)
i@124 191 # Get a list of files containing dark measurements
i@124 192 if dark_pattern != "":
i@124 193 dark_files = glob.glob(dark_pattern)
i@124 194
i@124 195 if dark_files:
i@124 196 logger.debug("Using %s as dark measurements files!" % ', '.join(dark_files))
i@124 197 measurement.dark_measurement = CustomLidarMeasurement(dark_files)
i@124 198 else:
i@124 199 logger.warning(
i@124 200 'No dark measurement files found when searching for %s. Will not use any dark measurements.' % dark_pattern)
i@124 201 try:
i@124 202 measurement = measurement.subset_by_scc_channels()
i@124 203 except ValueError as err:
i@124 204 logger.error(err)
i@124 205 sys.exit(1)
i@124 206
i@124 207 # Save the netcdf
i@124 208 logger.info("Saving netcdf")
i@124 209 measurement.set_measurement_id(measurement_id, measurement_number)
i@124 210 measurement.save_as_SCC_netcdf()
i@124 211 logger.info("Created file %s" % measurement.scc_filename)
i@124 212
i@124 213
ioannis@48 214 def main():
ioannis@48 215 # Define the command line argument
ioannis@56 216 parser = argparse.ArgumentParser(description="A program to convert Licel binary files to the SCC NetCDF format.")
ioannis@48 217 parser.add_argument("parameter_file", help="The path to a parameter file linking licel and SCC channels.")
i@124 218 parser.add_argument("files",
i@124 219 help="Location of licel files. Use relative path and filename wildcards. (default './*.*')",
victor@74 220 default="./*.*")
ioannis@48 221 parser.add_argument("-i", '--id_as_name',
ioannis@48 222 help="Use transient digitizer ids as channel names, instead of descriptive names",
ioannis@48 223 action="store_true")
ioannis@48 224 parser.add_argument("-m", "--measurement_id", help="The new measurement id", default=None)
ioannis@48 225 parser.add_argument("-n", "--measurement_number",
ioannis@48 226 help="The measurement number for the date from 00 to 99. Used if no id is provided",
ioannis@48 227 default="00")
ioannis@48 228 parser.add_argument("-t", "--temperature", type=float,
ioannis@48 229 help="The temperature (in C) at lidar level, required if using US Standard atmosphere",
ioannis@48 230 default="25")
ioannis@48 231 parser.add_argument("-p", "--pressure", type=float,
ioannis@48 232 help="The pressure (in hPa) at lidar level, required if using US Standard atmosphere",
ioannis@48 233 default="1020")
i@124 234 parser.add_argument('-D', '--dark_measurements',
i@124 235 help="Location of files containing dark measurements. Use relative path and filename wildcars, see 'files' parameter for example.",
i@101 236 default="", dest="dark_files"
i@124 237 )
i@101 238 parser.add_argument('--licel_timezone', help="String describing the timezone according to the tz database.",
i@101 239 default="UTC", dest="licel_timezone",
i@124 240 )
i@128 241 parser.add_argument('--cloudmask_settings', help="Experimental feature to automatically cloud mask measurements",
i@128 242 default="")
i@128 243
ioannis@55 244 # Verbosity settings from http://stackoverflow.com/a/20663028
ioannis@55 245 parser.add_argument('-d', '--debug', help="Print dubuging information.", action="store_const",
ioannis@55 246 dest="loglevel", const=logging.DEBUG, default=logging.INFO,
ioannis@55 247 )
ioannis@56 248 parser.add_argument('-s', '--silent', help="Show only warning and error messages.", action="store_const",
ioannis@55 249 dest="loglevel", const=logging.WARNING
ioannis@55 250 )
ulalume3@67 251 parser.add_argument('--version', help="Show current version.", action='store_true')
ioannis@55 252
ioannis@48 253 args = parser.parse_args()
ioannis@48 254
ioannis@55 255 # Get the logger with the appropriate level
ioannis@58 256 logging.basicConfig(format='%(levelname)s: %(message)s', level=args.loglevel)
ioannis@55 257 logger = logging.getLogger(__name__)
ioannis@55 258
i@101 259 # coloredlogs.install(fmt='%(levelname)s: %(message)s', level=args.loglevel)
ioannis@55 260
ulalume3@90 261 # Check for version
ulalume3@67 262 if args.version:
ulalume3@67 263 print("Version: %s" % __version__)
ulalume3@67 264 sys.exit(0)
ulalume3@67 265
ioannis@48 266 # Get a list of files to convert
victor@74 267 files = glob.glob(args.files)
ioannis@48 268
ulalume3@90 269 # If not files found, exit
ulalume3@90 270 if len(files) == 0:
ulalume3@90 271 logger.error("No files found when searching for %s." % args.files)
ulalume3@90 272 sys.exit(1)
ulalume3@90 273
ulalume3@90 274 # If everything OK, proceed
ulalume3@90 275 logger.info("Found {0} files matching {1}".format(len(files), os.path.abspath(args.files)))
ulalume3@90 276 CustomLidarMeasurement = create_custom_class(args.parameter_file, args.id_as_name, args.temperature,
i@101 277 args.pressure, args.licel_timezone)
ulalume3@90 278
i@128 279 if args.cloudmask_settings:
i@128 280 cloudmask_settings = read_cloudmask_settings_file(args.cloudmask_settings)
i@128 281
i@128 282 file_lists = get_cloud_free_files(CustomLidarMeasurement, files, cloudmask_settings)
ulalume3@90 283
i@124 284 for n, files in enumerate(file_lists):
i@124 285 measurement_id, measurement_no = get_corrected_measurement_id(args, n)
i@124 286 convert_to_scc(CustomLidarMeasurement, files, args.dark_files, measurement_id, measurement_no)
i@124 287 else:
i@124 288 convert_to_scc(CustomLidarMeasurement, files, args.dark_files, args.measurement_id, args.measurement_number)

mercurial