Fri, 16 Feb 2018 14:39:54 +0200
Attempt to add cloudmasking options.
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 | |
ioannis@48 | 10 | from ..licel import LicelLidarMeasurement |
ulalume3@67 | 11 | from ..__init__ import __version__ |
ioannis@48 | 12 | |
i@103 | 13 | logger = logging.getLogger(__name__) |
i@103 | 14 | |
ioannis@48 | 15 | |
i@101 | 16 | def create_custom_class(custom_netcdf_parameter_path, use_id_as_name=False, temperature=25., pressure=1020., |
i@101 | 17 | licel_timezone='UTC'): |
ioannis@48 | 18 | """ This funtion creates a custom LicelLidarMeasurement subclass, |
ioannis@48 | 19 | based on the input provided by the users. |
ioannis@48 | 20 | |
ioannis@48 | 21 | Parameters |
ioannis@48 | 22 | ---------- |
i@101 | 23 | custom_netcdf_parameter_path : str |
ioannis@48 | 24 | The path to the custom channels parameters. |
i@101 | 25 | use_id_as_name : bool |
ioannis@48 | 26 | Defines if channels names are descriptive or transient digitizer IDs. |
i@101 | 27 | temperature : float |
ioannis@48 | 28 | The ground temperature in degrees C (default 25.0). |
i@101 | 29 | pressure : float |
ioannis@48 | 30 | The ground pressure in hPa (default: 1020.0). |
i@101 | 31 | licel_timezone : str |
i@101 | 32 | String describing the timezone according to the tz database. |
ioannis@48 | 33 | |
ioannis@48 | 34 | Returns |
ioannis@48 | 35 | ------- |
ioannis@48 | 36 | CustomLidarMeasurement: |
ioannis@48 | 37 | A custom sub-class of LicelLidarMeasurement |
ioannis@48 | 38 | """ |
i@103 | 39 | logger.debug('Reading parameter files: %s' % custom_netcdf_parameter_path) |
ioannis@48 | 40 | custom_netcdf_parameters = read_settings_file(custom_netcdf_parameter_path) |
ioannis@48 | 41 | |
ioannis@48 | 42 | class CustomLidarMeasurement(LicelLidarMeasurement): |
ioannis@48 | 43 | extra_netcdf_parameters = custom_netcdf_parameters |
ioannis@48 | 44 | |
ulalume3@92 | 45 | def __init__(self, file_list=None): |
i@101 | 46 | super(CustomLidarMeasurement, self).__init__(file_list, use_id_as_name, licel_timezone=licel_timezone) |
ioannis@48 | 47 | |
ulalume3@92 | 48 | def set_PT(self): |
ioannis@48 | 49 | ''' Sets the pressure and temperature at station level. This is used if molecular_calc parameter is |
ioannis@48 | 50 | set to 0 (i.e. use US Standard atmosphere). |
ioannis@48 | 51 | |
ioannis@48 | 52 | The results are stored in the info dictionary. |
ioannis@48 | 53 | ''' |
ioannis@48 | 54 | |
ioannis@48 | 55 | self.info['Temperature'] = temperature |
ioannis@48 | 56 | self.info['Pressure'] = pressure |
ioannis@48 | 57 | |
ioannis@48 | 58 | return CustomLidarMeasurement |
ioannis@48 | 59 | |
ioannis@48 | 60 | |
ioannis@48 | 61 | def read_settings_file(settings_path): |
ioannis@48 | 62 | """ Read the settings file. |
ioannis@48 | 63 | |
ioannis@48 | 64 | The file should contain python code.""" |
ioannis@48 | 65 | if not os.path.isfile(settings_path): |
ioannis@58 | 66 | logging.error("The provided settings path does not correspond to a file.") |
ioannis@58 | 67 | sys.exit(1) |
ioannis@48 | 68 | |
ioannis@48 | 69 | dirname, basename = os.path.split(settings_path) |
ioannis@48 | 70 | sys.path.append(dirname) |
ioannis@48 | 71 | |
ioannis@48 | 72 | module_name, _ = os.path.splitext(basename) |
ioannis@48 | 73 | settings = importlib.import_module(module_name) |
ioannis@48 | 74 | return settings |
ioannis@48 | 75 | |
ioannis@48 | 76 | |
i@124 | 77 | def get_cloud_free_files(CustomLidarMeasurement, files, args): |
i@124 | 78 | logger.warning("Starting cloud mask procedure. This is an experimental feature.") |
i@124 | 79 | |
i@124 | 80 | try: |
i@124 | 81 | from cloudmask import cloudmask # Import here until we setup a proper installation procedure |
i@124 | 82 | except ImportError: |
i@124 | 83 | logger.error("Cloud mask module could not be loaded. Please install manually.") |
i@124 | 84 | sys.exit(1) |
i@124 | 85 | |
i@124 | 86 | measurement = CustomLidarMeasurement(files) |
i@124 | 87 | channel = measurement.channels[args.cloudmask_channel] |
i@124 | 88 | cloud_mask = cloudmask.CloudMaskRaw(channel) |
i@124 | 89 | idxs = cloud_mask.cloud_free_periods(args.cloudfree_period, args.cloud_search_height) |
i@124 | 90 | |
i@124 | 91 | if len(idxs) == 0: # If no cloud-free period found |
i@124 | 92 | logger.info('No cloud free period found. Nothing converted.') |
i@124 | 93 | sys.exit(1) |
i@124 | 94 | |
i@124 | 95 | logger.info("{0} cloud free period(s) found.".format(len(idxs))) |
i@124 | 96 | file_list = [] |
i@124 | 97 | for idx_min, idx_max in idxs: |
i@124 | 98 | current_files = measurement.files[idx_min:idx_max] |
i@124 | 99 | file_list.append(current_files) |
i@124 | 100 | |
i@124 | 101 | return file_list |
i@124 | 102 | |
i@124 | 103 | |
i@124 | 104 | def get_corrected_measurement_id(args, n): |
i@124 | 105 | """ Correct the provided measurement id, in case of multiple cloud-free periods. """ |
i@124 | 106 | if args.measurement_id is not None: |
i@124 | 107 | order = float(args.measurement_id[-2:]) |
i@124 | 108 | new_no = order + n |
i@124 | 109 | measurement_id = args.measurement_id[:-2] + str(new_no) |
i@124 | 110 | measurement_no = args.measurement_number # The same |
i@124 | 111 | else: |
i@124 | 112 | measurement_no = str(float(args.measurement_number) + n).zfill(2) |
i@124 | 113 | measurement_id = None |
i@124 | 114 | |
i@124 | 115 | return measurement_id, measurement_no |
i@124 | 116 | |
i@124 | 117 | |
i@124 | 118 | def convert_to_scc(CustomLidarMeasurement, files, dark_pattern, measurement_id, measurement_number): |
i@124 | 119 | """ Convert files to SCC. """ |
i@124 | 120 | measurement = CustomLidarMeasurement(files) |
i@124 | 121 | # Get a list of files containing dark measurements |
i@124 | 122 | if dark_pattern != "": |
i@124 | 123 | dark_files = glob.glob(dark_pattern) |
i@124 | 124 | |
i@124 | 125 | if dark_files: |
i@124 | 126 | logger.debug("Using %s as dark measurements files!" % ', '.join(dark_files)) |
i@124 | 127 | measurement.dark_measurement = CustomLidarMeasurement(dark_files) |
i@124 | 128 | else: |
i@124 | 129 | logger.warning( |
i@124 | 130 | 'No dark measurement files found when searching for %s. Will not use any dark measurements.' % dark_pattern) |
i@124 | 131 | try: |
i@124 | 132 | measurement = measurement.subset_by_scc_channels() |
i@124 | 133 | except ValueError as err: |
i@124 | 134 | logger.error(err) |
i@124 | 135 | sys.exit(1) |
i@124 | 136 | |
i@124 | 137 | # Save the netcdf |
i@124 | 138 | logger.info("Saving netcdf") |
i@124 | 139 | measurement.set_measurement_id(measurement_id, measurement_number) |
i@124 | 140 | measurement.save_as_SCC_netcdf() |
i@124 | 141 | logger.info("Created file %s" % measurement.scc_filename) |
i@124 | 142 | |
i@124 | 143 | |
ioannis@48 | 144 | def main(): |
ioannis@48 | 145 | # Define the command line argument |
ioannis@56 | 146 | parser = argparse.ArgumentParser(description="A program to convert Licel binary files to the SCC NetCDF format.") |
ioannis@48 | 147 | parser.add_argument("parameter_file", help="The path to a parameter file linking licel and SCC channels.") |
i@124 | 148 | parser.add_argument("files", |
i@124 | 149 | help="Location of licel files. Use relative path and filename wildcards. (default './*.*')", |
victor@74 | 150 | default="./*.*") |
ioannis@48 | 151 | parser.add_argument("-i", '--id_as_name', |
ioannis@48 | 152 | help="Use transient digitizer ids as channel names, instead of descriptive names", |
ioannis@48 | 153 | action="store_true") |
ioannis@48 | 154 | parser.add_argument("-m", "--measurement_id", help="The new measurement id", default=None) |
ioannis@48 | 155 | parser.add_argument("-n", "--measurement_number", |
ioannis@48 | 156 | help="The measurement number for the date from 00 to 99. Used if no id is provided", |
ioannis@48 | 157 | default="00") |
ioannis@48 | 158 | parser.add_argument("-t", "--temperature", type=float, |
ioannis@48 | 159 | help="The temperature (in C) at lidar level, required if using US Standard atmosphere", |
ioannis@48 | 160 | default="25") |
ioannis@48 | 161 | parser.add_argument("-p", "--pressure", type=float, |
ioannis@48 | 162 | help="The pressure (in hPa) at lidar level, required if using US Standard atmosphere", |
ioannis@48 | 163 | default="1020") |
i@124 | 164 | parser.add_argument('-D', '--dark_measurements', |
i@124 | 165 | help="Location of files containing dark measurements. Use relative path and filename wildcars, see 'files' parameter for example.", |
i@101 | 166 | default="", dest="dark_files" |
i@124 | 167 | ) |
i@101 | 168 | parser.add_argument('--licel_timezone', help="String describing the timezone according to the tz database.", |
i@101 | 169 | default="UTC", dest="licel_timezone", |
i@124 | 170 | ) |
i@124 | 171 | parser.add_argument('--cloudmask', help="Experimental feature to automatically cloud mask measurements", |
i@124 | 172 | default=False, action='store_true', |
i@124 | 173 | ) |
i@124 | 174 | parser.add_argument('--cloudmask_channel', help="Name of channel to apply the cloud mask.") |
i@124 | 175 | parser.add_argument('--cloudfree_period', type=float, help="Duration (in min) of cloud-free periods", |
i@124 | 176 | default="30", |
i@124 | 177 | ) |
i@124 | 178 | parser.add_argument('--cloud_search_height', type=float, help="Maximum altitude (in m) to check for clouds.", |
i@124 | 179 | default="12000", |
i@124 | 180 | ) |
ioannis@55 | 181 | # Verbosity settings from http://stackoverflow.com/a/20663028 |
ioannis@55 | 182 | parser.add_argument('-d', '--debug', help="Print dubuging information.", action="store_const", |
ioannis@55 | 183 | dest="loglevel", const=logging.DEBUG, default=logging.INFO, |
ioannis@55 | 184 | ) |
ioannis@56 | 185 | parser.add_argument('-s', '--silent', help="Show only warning and error messages.", action="store_const", |
ioannis@55 | 186 | dest="loglevel", const=logging.WARNING |
ioannis@55 | 187 | ) |
ulalume3@67 | 188 | parser.add_argument('--version', help="Show current version.", action='store_true') |
ioannis@55 | 189 | |
ioannis@48 | 190 | args = parser.parse_args() |
ioannis@48 | 191 | |
ioannis@55 | 192 | # Get the logger with the appropriate level |
ioannis@58 | 193 | logging.basicConfig(format='%(levelname)s: %(message)s', level=args.loglevel) |
ioannis@55 | 194 | logger = logging.getLogger(__name__) |
ioannis@55 | 195 | |
i@101 | 196 | # coloredlogs.install(fmt='%(levelname)s: %(message)s', level=args.loglevel) |
ioannis@55 | 197 | |
ulalume3@90 | 198 | # Check for version |
ulalume3@67 | 199 | if args.version: |
ulalume3@67 | 200 | print("Version: %s" % __version__) |
ulalume3@67 | 201 | sys.exit(0) |
ulalume3@67 | 202 | |
ioannis@48 | 203 | # Get a list of files to convert |
victor@74 | 204 | files = glob.glob(args.files) |
ioannis@48 | 205 | |
ulalume3@90 | 206 | # If not files found, exit |
ulalume3@90 | 207 | if len(files) == 0: |
ulalume3@90 | 208 | logger.error("No files found when searching for %s." % args.files) |
ulalume3@90 | 209 | sys.exit(1) |
ulalume3@90 | 210 | |
ulalume3@90 | 211 | # If everything OK, proceed |
ulalume3@90 | 212 | logger.info("Found {0} files matching {1}".format(len(files), os.path.abspath(args.files))) |
ulalume3@90 | 213 | CustomLidarMeasurement = create_custom_class(args.parameter_file, args.id_as_name, args.temperature, |
i@101 | 214 | args.pressure, args.licel_timezone) |
ulalume3@90 | 215 | |
i@124 | 216 | if args.cloudmask: |
i@124 | 217 | file_lists = get_cloud_free_files(CustomLidarMeasurement, files, args) |
ulalume3@90 | 218 | |
i@124 | 219 | for n, files in enumerate(file_lists): |
i@124 | 220 | measurement_id, measurement_no = get_corrected_measurement_id(args, n) |
i@124 | 221 | convert_to_scc(CustomLidarMeasurement, files, args.dark_files, measurement_id, measurement_no) |
i@124 | 222 | else: |
i@124 | 223 | convert_to_scc(CustomLidarMeasurement, files, args.dark_files, args.measurement_id, args.measurement_number) |