From 7ab9cd234be937b2852394e867214d4608e63b9d Mon Sep 17 00:00:00 2001 From: crangelsmith Date: Tue, 16 Aug 2022 16:32:18 +0100 Subject: [PATCH 1/5] updated config generator script to create config files from a parquet file --- pyveg/configs/config_template.py | 31 +- pyveg/requirements.txt | 1 + pyveg/scripts/generate_config_file.py | 535 +++++++++++--------------- 3 files changed, 230 insertions(+), 337 deletions(-) diff --git a/pyveg/configs/config_template.py b/pyveg/configs/config_template.py index 9e1915f7..ca67c1b0 100644 --- a/pyveg/configs/config_template.py +++ b/pyveg/configs/config_template.py @@ -12,34 +12,24 @@ # Define location to save all outputs. Note that every time the pipeline job # is rerun, a datestamp will be appended to the output_location. output_location = "OUTPUT_LOCATION" -output_location_type = "OUTPUT_LOCATION_TYPE" +output_location_type = "local" # parse selection. Note (long, lat) GEE convention. -coordinates = [LEFT, BOTTOM, RIGHT, TOP] - -# optional coords_id setting -COORDS_ID_STRING - -# pattern_type description -pattern_type = "PATTERN_TYPE" +bounds = [LEFT, BOTTOM, RIGHT, TOP] date_range = ["START_DATE", "END_DATE"] # From the dictionary entries in data_collections.py, which shall we use # (these will become "Sequences") -collections_to_use = ["COLLECTION_NAME", "WEATHER_COLL_NAME"] +collections_to_use = ["COLLECTION_NAME"] # Dictionary defining what Modules should run in each Sequence. modules_to_use = { - "WEATHER_COLL_NAME": ["WeatherDownloader", "WeatherImageToJSON"], "COLLECTION_NAME": [ "VegetationDownloader", "VegetationImageProcessor", - "NetworkCentralityCalculator", - "NDVICalculator", ], - "combine": ["VegAndWeatherJsonCombiner"], } # The following demonstrates how parameters can be set for individual Modules: @@ -47,19 +37,4 @@ "COLLECTION_NAME": { "time_per_point": "TIME_PER_POINT" }, - "WEATHER_COLL_NAME": { - "time_per_point": "TIME_PER_POINT", - "date_range": ["WEATHER_STARTDATE", "END_DATE"] - }, - "VegetationDownloader": {"region_size": REGION_SIZE}, - "VegetationImageProcessor": {"run_mode": "RUN_MODE"}, - "NetworkCentralityCalculator": { - "n_threads": NUM_THREADS, - "run_mode": "RUN_MODE", - "n_sub_images": NUM_SUBIMAGES - }, - "NDVICalculator": { - "run_mode": "RUN_MODE", - "n_sub_images": NUM_SUBIMAGES - } } diff --git a/pyveg/requirements.txt b/pyveg/requirements.txt index 9f24a2a1..0cbea48e 100644 --- a/pyveg/requirements.txt +++ b/pyveg/requirements.txt @@ -25,3 +25,4 @@ pypandoc mdutils pandoc sphinx_rtd_theme +pyarrow diff --git a/pyveg/scripts/generate_config_file.py b/pyveg/scripts/generate_config_file.py index 8566b1fc..2b369d10 100644 --- a/pyveg/scripts/generate_config_file.py +++ b/pyveg/scripts/generate_config_file.py @@ -35,9 +35,9 @@ import re import time +import geopandas + from pyveg.configs import collections -from pyveg.coordinates import coordinate_store -from pyveg.src.coordinate_utils import lookup_country def get_template_text(): @@ -49,51 +49,35 @@ def get_template_text(): return open(template_filepath).read() -def make_output_location(coords_id, collection_name, latitude, longitude, country): - # quite restricted on characters allowed in Azure container names - - # use NSEW rather than negative numbers in coordinates - if latitude.startswith("-"): - latitude = latitude[1:] + "S" - else: - latitude = latitude + "N" - if longitude.startswith("-"): - longitude = longitude[1:] + "W" - else: - longitude = longitude + "E" - +def make_output_location(coords_id, collection_name, left, bottom, right, top): if coords_id: - output_location = ( - f"{coords_id}-{collection_name}-{latitude}-{longitude}-{country}" - ) + output_location = f"{coords_id}-{collection_name}-{left}-{bottom}-{right}-{top}" else: - output_location = f"{collection_name}-{latitude}-{longitude}-{country}" + output_location = f"{collection_name}-{left}-{bottom}-{right}-{top}" return output_location def make_filename( configs_dir, - test_mode, - longitude, - latitude, - country, - pattern_type, + left, + bottom, + right, + top, start_date, end_date, time_per_point, - region_size, collection_name, - run_mode, coords_id, ): """ Construct a filename from the specified parameters. """ - filename_start = "testconfig" if test_mode else "config" + filename_start = "config" if coords_id: - filename_start += "_" + coords_id + filename_start += "_" + str(coords_id) filepath = os.path.join( configs_dir, - f"{filename_start}_{collection_name}_{latitude}N_{longitude}E_{country}_{region_size}_{pattern_type}_{start_date}_{end_date}_{time_per_point}_{run_mode}.py", + f"{filename_start}_{collection_name}_{left}_{bottom}_{right}_{top}_{start_date}_{end_date}_{time_per_point}.py", ) return filepath @@ -101,18 +85,14 @@ def make_filename( def write_file( configs_dir, output_location, - longitude, - latitude, - country, - pattern_type, + left, + bottom, + right, + top, start_date, end_date, time_per_point, - region_size, collection_name, - run_mode, - n_threads, - test_mode=False, coords_id=None, ): """ @@ -120,57 +100,32 @@ def write_file( """ filename = make_filename( configs_dir, - test_mode, - longitude, - latitude, - country, - pattern_type, + left, + bottom, + right, + top, start_date, end_date, time_per_point, - region_size, collection_name, - run_mode, coords_id, ) - if time_per_point.endswith("d") or time_per_point.endswith("w"): - weather_collection_name = "ERA5_daily" - weather_start_date = start_date - else: - weather_collection_name = "ERA5" - if test_mode: - weather_start_date = start_date - else: - # also include historical weather data - weather_start_date = collections.data_collections[weather_collection_name][ - "min_date" - ] - text = get_template_text() current_time = time.strftime("%y-%m-%d %H:%M:%S") text = text.replace("CURRENT_TIME", current_time) - output_location_type = "azure" if run_mode == "batch" else "local" text = text.replace("COLLECTION_NAME", collection_name) - text = text.replace("WEATHER_COLL_NAME", weather_collection_name) - text = text.replace("OUTPUT_LOCATION_TYPE", output_location_type) text = text.replace("OUTPUT_LOCATION", output_location) - text = text.replace("LATITUDE", latitude) - text = text.replace("LONGITUDE", longitude) - text = text.replace("PATTERN_TYPE", pattern_type) + text = text.replace( + "RIGHT", str(int(right)) + ) # hacky way of removing unnecesary zeros + text = text.replace("LEFT", str(int(left))) + text = text.replace("TOP", str(int(top))) + text = text.replace("BOTTOM", str(int(bottom))) text = text.replace("START_DATE", start_date) - text = text.replace("WEATHER_STARTDATE", weather_start_date) text = text.replace("END_DATE", end_date) text = text.replace("TIME_PER_POINT", time_per_point) - text = text.replace("REGION_SIZE", region_size) - text = text.replace("RUN_MODE", run_mode) - text = text.replace("NUM_THREADS", str(n_threads)) - n_subimages = "10" if test_mode else "-1" - text = text.replace("NUM_SUBIMAGES", n_subimages) - if coords_id: - text = text.replace("COORDS_ID_STRING", 'coords_id = "{}"'.format(coords_id)) - else: - text = text.replace("COORDS_ID_STRING", "") + with open(filename, "w") as configfile: configfile.write(text) print( @@ -188,13 +143,12 @@ def main(): for k in collections.data_collections.keys() if collections.data_collections[k]["data_type"] == "vegetation" ] - run_modes = ["local", "batch"] date_regex = re.compile("[\d]{4}-[01][\d]-[0123][\d]") time_per_point_regex = re.compile("[\d]+[dwmy]") - lat_range = [-90.0, 90.0] - long_range = [-180.0, 180.0] - n_threads_range = range(1, 17) - default_n_threads = 4 + left_bound = [0, 700000] + bottom_bound = [0, 1300000] + top_bound = [0, 1300000] + right_bound = [0, 700000] # create argument parser in case user wants to use command line args parser = argparse.ArgumentParser( @@ -203,58 +157,34 @@ def main(): """ ) parser.add_argument( - "--coords_id", help="(optional) ID of location in coordinates.py", type=str + "--bounds_file", help="Path to parket file with coordinate chips", type=str ) parser.add_argument( "--configs_dir", help="path to directory containing config files" ) - parser.add_argument("--collection_name", help="collection name (e.g. 'Sentinel2')") parser.add_argument( - "--output_dir", help="Directory for local output data", type=str + "--collection_name", + help="collection name (e.g. 'Sentinel2')", + default="Sentinel2", ) parser.add_argument( - "--test_mode", - help="Run in test mode, over fewer months and with fewer sub-images", - action="store_true", + "--output_dir", help="Directory for local output data", type=str ) - parser.add_argument("--latitude", help="latitude in degrees N", type=float) - parser.add_argument("--longitude", help="longitude in degrees E", type=float) - parser.add_argument("--country", help="Country of location", type=str) + parser.add_argument("--left", help="left bound in Eastings", type=float) + parser.add_argument("--right", help="right bound in Eastings", type=float) + parser.add_argument("--bottom", help="bottom bound in Nothings", type=float) + parser.add_argument("--top", help="top bound in Nothings", type=float) parser.add_argument("--start_date", help="start date, format YYYY-MM-DD", type=str) parser.add_argument("--end_date", help="end date, format YYYY-MM-DD", type=str) parser.add_argument( "--time_per_point", help="frequency of image, e.g. '1m', '1w'", type=str ) - parser.add_argument( - "--region_size", - help="Size of region to download, in degrees lat/long", - type=float, - ) - parser.add_argument( - "--pattern_type", - help="Type of patterned vegetation, e.g. 'spots', 'labyrinths'", - type=str, - ) - parser.add_argument( - "--run_mode", - help=""" - 'local' for running on local machine, 'azure' for running some time-consuming parts (i.e. vegetation image processing) on Azure batch - """, - type=str, - ) - parser.add_argument( - "--n_threads", - help=""" - How many threads (cores) to parallelize some processing functions over - """, - type=int, - ) args = parser.parse_args() # sanity check - if args.coords_id and (args.latitude or args.longitude): - print("Please select EITHER coords_id OR latitude/longitude") + if args.bounds_file and (args.bottom or args.right or args.left or args.right): + print("Please select EITHER coords_id OR bounds") return ############# @@ -278,14 +208,6 @@ def main(): if len(configs_dir) == 0: configs_dir = default_configs_dir - # test mode - test_mode = args.test_mode if args.test_mode else False - if not test_mode: - do_test = input( - "Would you like to make a test config file, with fewer months, and only a subset of sub-images? Press 'y' if so, or press Return for a normal config. : " - ) - test_mode = do_test.startswith("y") or do_test.startswith("Y") - # collection name collection_name = args.collection_name if args.collection_name else None while not collection_name in collection_names: @@ -294,73 +216,9 @@ def main(): collection_names ) ) - - # (optional) ID from coordinates.py - coords_id = args.coords_id if args.coords_id else None - latitude = None - longitude = None - country = None - region_size = None - pattern_type = None - if coords_id: - try: - row = coordinate_store.loc[coords_id] - latitude = row["latitude"] - longitude = row["longitude"] - country = row["country"] - region_size = row["region_size"] - pattern_type = row["type"] - except (KeyError): - print("Unknown id {} - please enter coordinates manually".format(coords_id)) - - # latitude and longitude - if not latitude: - latitude = args.latitude if args.latitude else -999.0 - - while not ( - isinstance(latitude, float) - and latitude > lat_range[0] - and latitude < lat_range[1] - ): - latitude = float( - input( - "please enter Latitude (degrees N) in the range {} : ".format( - lat_range - ) - ) - ) - if not longitude: - longitude = args.longitude if args.longitude else -999.0 - while not ( - isinstance(longitude, float) - and longitude > long_range[0] - and longitude < long_range[1] - ): - longitude = float( - input( - "please enter Longitude (degrees E) in the range {} : ".format( - long_range - ) - ) - ) - - # country - country = args.country if args.country else "" - if not country: - country = input( - "Enter name of country, or press return to use OpenCage country lookup based on coordinates : " - ) - if len(country) == 0: - country = lookup_country(latitude, longitude) - # remove spaces - country = re.sub("[\s]+", "", country) - # start date start_date = args.start_date if args.start_date else "" - if test_mode: - default_start_date = "2019-01-01" - else: - default_start_date = collections.data_collections[collection_name]["min_date"] + default_start_date = collections.data_collections[collection_name]["min_date"] while not date_regex.search(start_date): start_date = input( "Enter start date in format YYYY-MM-DD, or press Return for default ({}) : ".format( @@ -372,10 +230,7 @@ def main(): # end date end_date = args.end_date if args.end_date else "" - if test_mode: - default_end_date = "2019-03-01" - else: - default_end_date = collections.data_collections[collection_name]["max_date"] + default_end_date = collections.data_collections[collection_name]["max_date"] while not date_regex.search(end_date): end_date = input( "Enter end date in format YYYY-MM-DD, or press Return for default ({}) : ".format( @@ -398,146 +253,208 @@ def main(): ) if len(time_per_point) == 0: time_per_point = default_time_per_point - - # region size - if not region_size: - region_size = args.region_size if args.region_size else -1.0 - default_region_size = 0.08 - while not ( - isinstance(region_size, float) and region_size > 0.0 and region_size <= 0.08 - ): - region_size = input( - "Enter region size in degrees latitude/longitude, or press Return for max/default ({}) : ".format( - default_region_size - ) - ) - if len(region_size) == 0: - region_size = default_region_size - else: - region_size = float(region_size) - # now we've established it fulfils the requirements, convert to a str - region_size = str(region_size) - - # pattern_type - if not pattern_type: - pattern_type = args.pattern_type if args.pattern_type else "" - default_pattern_type = "unknown" - while len(pattern_type) < 1: - pattern_type = input( - "Enter type of patterned vegetation (e.g. 'spots', 'labyrinths', or press Return for default ('{}') : ".format( - default_pattern_type - ) - ) - if len(pattern_type) == 0: - pattern_type = default_pattern_type - pattern_type = pattern_type.replace(" ", "-").lower() - - # run mode - run_mode = args.run_mode if args.run_mode else "" - default_run_mode = "local" - while not run_mode in run_modes: - run_mode = input( - "Would you like time-consuming functions to be run on the cloud? Choose from the following: {}, or press Return for default option '{}': ".format( - run_modes, default_run_mode - ) - ) - if len(run_mode) == 0: - run_mode = default_run_mode - - # output directory + # output directory output_dir = args.output_dir if args.output_dir else "" - if run_mode == "local" and not output_dir: + if not output_dir: output_dir = input( "Enter location for output, or press Return for default ('.') : " ) if len(output_dir) == 0: output_dir = "." - lat_string = "{:.2f}".format(latitude) - long_string = "{:.2f}".format(longitude) - output_location = make_output_location( - coords_id, collection_name, lat_string, long_string, country - ) + # (optional) ID from coordinates.py + bounds_file = args.bounds_file if args.bounds_file else None + left = None + right = None + bottom = None + top = None + if bounds_file: + chips_gdf = geopandas.read_parquet(bounds_file) + chips_gdf.to_crs("EPSG:27700") + + if "on_land" in chips_gdf: + index = chips_gdf[chips_gdf["on_land"] == True].index + else: + index = chips_gdf.index + + for i in index: + row = chips_gdf.iloc[i] + bottom = int(row["geometry"].bounds[1]) + left = int(row["geometry"].bounds[0]) + right = int(row["geometry"].bounds[2]) + top = int(row["geometry"].bounds[3]) + + left_string = "{:0>6}".format(left) + bottom_string = "{:0>7}".format(bottom) + right_string = "{:0>6}".format(right) + top_string = "{:0>7}".format(top) + + output_location = make_output_location( + i, collection_name, left_string, bottom_string, right_string, top_string + ) - if run_mode == "local": - output_location = os.path.join(output_dir, output_location) + output_location = os.path.join(output_dir, output_location) + + print( + """ + output_location {} + collection: {} + left: {} + bottom: {} + right: {} + top: {} + start_date: {} + end_date: {} + time_per_point: {} + """.format( + output_location, + collection_name, + left_string, + bottom_string, + right_string, + top_string, + start_date, + end_date, + time_per_point, + ) + ) + + config_filename = write_file( + configs_dir, + output_location, + left_string, + bottom_string, + right_string, + top_string, + start_date, + end_date, + time_per_point, + collection_name, + i, + ) - # num threads - n_threads = args.n_threads if args.n_threads else 0 - while not (isinstance(n_threads, int) and n_threads in n_threads_range): - if run_mode == "local": - n_threads = input( - "How many threads would you like time-consuming processing functions to use? (Many computers will have 4 or 8 threads available). Press return for default value {} : ".format( - default_n_threads + print( + """ + To run pyveg using this configuration, do: + + pyveg_run_pipeline --config_file {} + """.format( + config_filename ) ) - if len(n_threads) == 0: - n_threads = default_n_threads - else: - try: - n_threads = int(n_threads) - except: - print("Please enter an integer value") - else: - n_threads = 1 + else: + # bounds + if not left: + left = args.left if args.left else 0 + + while not ( + isinstance(left, float) + and left >= left_bound[0] + and left <= left_bound[1] + ): + left = float( + input( + "please enter left bound (eastings) in the range {} : ".format( + left_bound[1] + ) + ) + ) + if not right: + right = args.right if args.right else 0 + while not ( + isinstance(right, float) + and right >= right_bound[0] + and right <= right_bound[1] + ): + right = float( + input( + "please enter right bound (eastings) in the range {} : ".format( + right[1] + ) + ) + ) + if not top: + top = args.top if args.top else 0 + while not ( + isinstance(top, float) and top >= top_bound[0] and top <= top_bound[1] + ): + top = float( + input( + "please enter top bound (degrees northings) in the range {} : ".format( + top_bound[1] + ) + ) + ) + if not bottom: + bottom = args.bottom if args.bottom else 0 + while not ( + isinstance(bottom, float) + and bottom >= bottom_bound[0] + and top <= bottom_bound[1] + ): + bottom = float( + input( + "please enter bottom bound (degrees northings) in the range {} : ".format( + bottom_bound[1] + ) + ) + ) - print( - """ - output_location {} - collection: {} - latitude: {} - longitude: {} - country: {} - pattern_type: {} - start_date: {} - end_date: {} - time_per_point: {} - region_size: {} - run_mode: {} - n_threads: {} - """.format( + left_string = "{:0>6}".format(left) + bottom_string = "{:0>7}".format(bottom) + right_string = "{:0>6}".format(right) + top_string = "{:0>7}".format(top) + + output_location = make_output_location( + None, collection_name, left_string, bottom_string, right_string, top_string + ) + + print( + """ + output_location {} + collection: {} + left: {} + bottom: {} + right: {} + top: {} + start_date: {} + end_date: {} + time_per_point: {} + """.format( + output_location, + collection_name, + left_string, + bottom_string, + right_string, + top_string, + start_date, + end_date, + time_per_point, + ) + ) + + config_filename = write_file( + configs_dir, output_location, - collection_name, - lat_string, - long_string, - country, - pattern_type, + left_string, + bottom_string, + right_string, + top_string, start_date, end_date, time_per_point, - region_size, - run_mode, - n_threads, + collection_name, ) - ) + print( + """ + To run pyveg using this configuration, do: - config_filename = write_file( - configs_dir, - output_location, - long_string, - lat_string, - country, - pattern_type, - start_date, - end_date, - time_per_point, - region_size, - collection_name, - run_mode, - n_threads, - test_mode, - coords_id, - ) - print( - """ -To run pyveg using this configuration, do: - -pyveg_run_pipeline --config_file {} + pyveg_run_pipeline --config_file {} -""".format( - config_filename + """.format( + config_filename + ) ) - ) if __name__ == "__main__": From 86c9a87bf58ac319da2bdd607eae62e9370b12c3 Mon Sep 17 00:00:00 2001 From: crangelsmith Date: Wed, 17 Aug 2022 15:36:55 +0100 Subject: [PATCH 2/5] adding script to run download in a loop --- pyveg/scripts/run_pipeline_loop.py | 73 ++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 pyveg/scripts/run_pipeline_loop.py diff --git a/pyveg/scripts/run_pipeline_loop.py b/pyveg/scripts/run_pipeline_loop.py new file mode 100644 index 00000000..5ef5311f --- /dev/null +++ b/pyveg/scripts/run_pipeline_loop.py @@ -0,0 +1,73 @@ +import argparse +import logging +import shlex +import subprocess +import time +from logging.handlers import RotatingFileHandler +from os import listdir +from os.path import isfile, join + +logger = logging.getLogger("pyveg_bulk_donwload_job") +formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") +logger.setLevel(logging.INFO) + +c_handler = logging.StreamHandler() +c_handler.setFormatter(formatter) +f_handler = RotatingFileHandler( + "pyveg_bulk_download_job_{}.log".format(time.strftime("%Y-%m-%d_%H-%M-%S")), + maxBytes=5 * 1024 * 1024, + backupCount=10, +) +f_handler.setFormatter(formatter) + +logger.addHandler(f_handler) +logger.addHandler(c_handler) + + +def run_pipeline(config_directory): + # Directory containing input files + + # Get the full paths to the files in the input dir + full_paths = [ + join(config_directory, f) + for f in listdir(config_directory) + if isfile(join(config_directory, f)) + ] + + # Build a list of commands to reproject each file individually + cmds = [] + for input_fpath in full_paths[:5]: + safe_input = shlex.quote(str(input_fpath)) + + cmds.append(f"pyveg_run_pipeline --config_file {safe_input}") + + # Now run those commands (This might be expensive) + + failed = 0 + for cmd in cmds: + logger.info(f"Running gee download using the command: {cmd}") + try: + subprocess.run(cmd, shell=True) + + except subprocess.SubprocessError as e: + failed += 1 + logger.error(f"Download using the command: {cmd} failed") + + return failed + + +def main(): + + # run pyveg pipeline on a loop by running all the config files in a given directory + parser = argparse.ArgumentParser() + parser.add_argument( + "--config_dir", help="Path to directory with config files", required=True + ) + + args = parser.parse_args() + n = run_pipeline(args.config_dir) + logger.info(f"Bulk download finished. Number of failed dowloads {n}") + + +if __name__ == "__main__": + main() From 5719b89bc81587b92f53b64153bb19add0e6b2f8 Mon Sep 17 00:00:00 2001 From: crangelsmith Date: Thu, 18 Aug 2022 12:10:56 +0100 Subject: [PATCH 3/5] updating documentation --- pyveg/README.md | 64 ++++++++++++++++++++--------- pyveg/scripts/run_pipeline_loop.py | 2 +- pyveg/testdata/images_1024.parquet | Bin 0 -> 252695 bytes setup.py | 1 + 4 files changed, 46 insertions(+), 21 deletions(-) create mode 100644 pyveg/testdata/images_1024.parquet diff --git a/pyveg/README.md b/pyveg/README.md index 59e69742..927cdc93 100644 --- a/pyveg/README.md +++ b/pyveg/README.md @@ -64,9 +64,19 @@ The download job is fully specified by a configuration file, which you point to Note that we use the [BNG convention](https://britishnationalgrid.uk/) for coordinates, i.e. `(eastings,northings)` and we set bounds for regions to download in the convention `(left, bottom, right, top)`. -#### Generating a download configuration file +### Downloading data on a loop from multiple configuration files + +You might want to download images from several configuration files in one go. This can be done with the following command: + +``` +pyveg_run_pipeline_loop --config_dir configs +``` + +where `config_dir` is the path to a directory where all the config files you want to run are found. The script runs a loop and +exectues the `pyveg_run_pipeline` command on each available file found in that path. -**TODO: To be updated with peep instructions!** + +#### Generating a download configuration file To create a configuration file for use in the pyveg pipeline described above, use the command ``` @@ -83,11 +93,13 @@ this allows the user to specify various characteristics of the data they want to * Landsat5: [Available from 1984-03-10 to 2013-01-31 at 60m resolution.](https://developers.google.com/earth-engine/datasets/catalog/LANDSAT_LT05_C01_T1) * Landsat4: [Available from 1982-07-16 to 1993-12-14 at 60m resolution.](https://developers.google.com/earth-engine/datasets/catalog/LANDSAT_LT04_C01_T1) -* `--latitude`: The latitude (in degrees north) of the centre point of the image collection. +* `--left`: The left bound of the region to be downloaded (in Easting BNG coordinates) . + +* `--bottom`: The bottom bound of the region to be downloaded (in Northing BNG coordinates) . -* `--longitude`: The longitude (in degrees east) of the centre point of the image collection. +* `--right`: The right bound of the region to be downloaded (in Easting BNG coordinates) . -* `--country`: The country (for the file name) can either be entered, or use the specified coordinates to look up the country name from the OpenCage database. +* `--top`: The top bound of the region to be downloaded (in Northing BNG coordinates) . * `--start_date`: The start date in the format ‘YYYY-MM-DD’, the default is ‘2015-01-01’ (or ‘2019-01-01’ for a test config file). @@ -95,36 +107,48 @@ this allows the user to specify various characteristics of the data they want to * `--time_per_point`: The option to run the image collection either monthly (‘1m’) or weekly (‘1w’), with the default being monthly. -* `--run_mode`: The option to run time-consuming functions on Azure (‘batch’) or running locally on your own computer (‘local’). The default is local. For info about running on Azure go [here](UsingAzure.md). - * `--output_dir`: The option to write the output to a specified directory, with the default being the current directory. -* `--test_mode`: The option to make a test config file, containing fewer months and a subset of sub-images, with a default option to have a normal config file. - * By choosing the test config file, the start and end dates (see below) are defaulted to cover a smaller time span. - * It is recommended that the test config option should be used purely to determine if the options specified by the user are correct. - - -* `--n_threads`: Finally, how many threads the user would like to use for the time-consuming processes, either 4 (default) or 8. - For example: ``` - pyveg_generate_config --configs_dir "pyveg/configs" --collection_name "Sentinel2" --latitude 11.58 --longitude 27.94 --start_date "2016-01-01" --end_date "2020-06-30" --time_per_point "1m" --run_mode "local" --n_threads 4 + pyveg_generate_config --configs_dir "pyveg/configs" --collection_name "Sentinel2" --left 419840 --bottom 0235520 --right 430080 --top 0245760 --start_date "2016-01-01" --end_date "2016-02-01" --time_per_point "1m" ``` -This generates a file named `config_Sentinel2_11.58N_27.94E_Sudan_2016-01-01_2020-06-30_1m_local.py` along with instructions on how to use this configuration file to download data through the pipeline, in this case the following: +This generates a file named `config_Sentinel2_419840_0235520_430080_0245760_2016-01-01_2016-02-01_1m_local.py` along with instructions on how to use this configuration file to download data through the pipeline, in this case the following: ``` -pyveg_run_pipeline --config_file pyveg/configs/config_Sentinel2_11.58N_27.94E_Sudan_2016-01-01_2020-06-30_1m_local.py +pyveg_run_pipeline --config_file pyveg/configs/config_Sentinel2_419840_0235520_430080_0245760_2016-01-01_2016-02-01_1m_local.py ``` Individual options can be specified by the user via prompt. The options for this can be found by typing ```pyveg_generate_config --help```. +#### Generating many configuration files from a geoparquet file -### More Details on Downloading +If you want to generate a large number of configuration files from a dataset of geometries you can do this using the command `pyveg_generate_config` and +a [geoparquet](https://pypi.org/project/geoparquet/) file (`bounds_file`). + +The geoparquet file must contain a column named `geometry`, and each row correspond to an individual geometry (an example of this +file can be found as `images_1024.parquet` on the `testdata` directory). If provided +the `--bounds_file` flag, the `pyveg_generate_config` script will read the geoparquet file, loop over its rows creating a config file +for each geometry. + +If a column `on_land` is available in the geoparquet file, the script will filter only rows where the column is True. +If not this column is not found, the scrip will loop over all rows of the data. + +Flags such as `start_date`, `end_date`, `time_per_point` must be provided as well, and it will be used for all the config files created. + +Flags such as `configs_dir`, `output_dir` are optional, defining to write the output of the config files or downloads to a specified directory +with the default being the current directory. -During the download job, `pyveg` will break up your specified date range into a time series, and download data at each point in the series. Note that by default the vegetation images downloaded from GEE will be split up into 50x50 pixel images, vegetation metrics are then calculated on the sub-image level. Both colour (RGB) and Normalised Difference Vegetation Index (NDVI) images are downloaded and stored. Vegetation metrics include the mean NDVI pixel intensity across sub-images, and also network centrality metrics, discussed in more detail below. +An example: + +``` +pyveg_generate_config --bounds_file testdata/images_1024.parquet --start_date 2018-04-02 --end_date 2018-10-01 --time_per_point 5m --configs_dir configs --output_dir output_dowloads +``` + +### More Details on Downloading -For weather collections e.g. the ERA5, due to coarser resolution, the precipitation and temperature "images" are averaged into a single value at each point in the time series. +During the download job, `pyveg` will break up your specified date range into a time series defined by the `time_per_point` flag , and download data at each point in the series. Note that by default the images downloaded from GEE will be split up into 32x32 pixel images. Both colour (RGB) and a mosaic with counts of images used in the composite (COUNT) images are downloaded and stored. ### Rerunning partially succeeded jobs diff --git a/pyveg/scripts/run_pipeline_loop.py b/pyveg/scripts/run_pipeline_loop.py index 5ef5311f..864a18a3 100644 --- a/pyveg/scripts/run_pipeline_loop.py +++ b/pyveg/scripts/run_pipeline_loop.py @@ -36,7 +36,7 @@ def run_pipeline(config_directory): # Build a list of commands to reproject each file individually cmds = [] - for input_fpath in full_paths[:5]: + for input_fpath in full_paths: safe_input = shlex.quote(str(input_fpath)) cmds.append(f"pyveg_run_pipeline --config_file {safe_input}") diff --git a/pyveg/testdata/images_1024.parquet b/pyveg/testdata/images_1024.parquet new file mode 100644 index 0000000000000000000000000000000000000000..72fb3471255b7716b05ec5d0913ec20e2c750745 GIT binary patch literal 252695 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 literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py index e7bf97bd..b70f98f8 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ "pyveg_generate_config=pyveg.scripts.generate_config_file:main", "pyveg_create_analysis_report=pyveg.scripts.create_analysis_report:main", "pyveg_analysis_summary_data=pyveg.scripts.analyse_pyveg_summary_data:main", + "pyveg_run_pipeline_loop=pyveg.scripts.run_pipeline_loop:main", ] }, ) From a0f31a470fcdbfd4173feb411679d93f91258f16 Mon Sep 17 00:00:00 2001 From: Andy Smith Date: Thu, 1 Sep 2022 19:26:12 +0100 Subject: [PATCH 4/5] rename `chips_gdf` to `bounds_gdf` --- pyveg/scripts/generate_config_file.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pyveg/scripts/generate_config_file.py b/pyveg/scripts/generate_config_file.py index 2b369d10..020850f1 100644 --- a/pyveg/scripts/generate_config_file.py +++ b/pyveg/scripts/generate_config_file.py @@ -157,7 +157,9 @@ def main(): """ ) parser.add_argument( - "--bounds_file", help="Path to parket file with coordinate chips", type=str + "--bounds_file", + help="Path to a geoparket file. The file should include a geometry column (named 'geometry'). A config file will be created for each row in the geoparket file.", + type=str, ) parser.add_argument( "--configs_dir", help="path to directory containing config files" @@ -269,16 +271,16 @@ def main(): bottom = None top = None if bounds_file: - chips_gdf = geopandas.read_parquet(bounds_file) - chips_gdf.to_crs("EPSG:27700") + bounds_gdf = geopandas.read_parquet(bounds_file) + bounds_gdf.to_crs("EPSG:27700") - if "on_land" in chips_gdf: - index = chips_gdf[chips_gdf["on_land"] == True].index + if "on_land" in bounds_gdf: + index = bounds_gdf[bounds_gdf["on_land"] == True].index else: - index = chips_gdf.index + index = bounds_gdf.index for i in index: - row = chips_gdf.iloc[i] + row = bounds_gdf.iloc[i] bottom = int(row["geometry"].bounds[1]) left = int(row["geometry"].bounds[0]) right = int(row["geometry"].bounds[2]) From d28c7333c7c05a37b008e66e776c49771df1662a Mon Sep 17 00:00:00 2001 From: Camila Rangel Smith Date: Fri, 2 Sep 2022 09:50:33 +0100 Subject: [PATCH 5/5] removing unneeded test --- pyveg/tests/test_image_utils.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pyveg/tests/test_image_utils.py b/pyveg/tests/test_image_utils.py index 4d78e53e..9212858b 100644 --- a/pyveg/tests/test_image_utils.py +++ b/pyveg/tests/test_image_utils.py @@ -67,17 +67,6 @@ def test_compare_opposite_images(): assert compare_binary_images(img1, img2) < 0.1 -def test_create_gif_from_images(): - path_dir = os.path.join(os.path.dirname(__file__), "..", "testdata/") - create_gif_from_images(path_dir, "test", "black_and_white") - list_png_files = [ - f - for f in os.listdir(path_dir) - if (os.path.isfile(os.path.join(path_dir, f)) and f == "test.gif") - ] - assert len(list_png_files) == 1 - - def test_pillow_to_numpy(): img = Image.open( os.path.join(os.path.dirname(__file__), "..", "testdata", "white.png")