Thu, 22 Mar 2018 13:53:18 +0200
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) |