Thu, 13 Sep 2018 13:59:32 +0300
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) |