atmospheric_lidar/scripts/licel2qa.py

Thu, 13 Sep 2018 13:59:32 +0300

author
Iannis B <ioannis@inoe.ro>
date
Thu, 13 Sep 2018 13:59:32 +0300
changeset 153
24ce9e10906c
permissions
-rw-r--r--

Start of telecover conversion function (just for merging).

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

mercurial