diff --git a/.gitignore b/.gitignore index 43514da4..12df06c4 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,5 @@ cython_debug/ # Input files local/rest_api_gcbm/input/ +GCBM_Demo_Run/ +GCBM_New_Demo_Run/ diff --git a/GCBM_New_Demo_Run.zip b/GCBM_New_Demo_Run.zip index ec0589f1..250cb646 100644 Binary files a/GCBM_New_Demo_Run.zip and b/GCBM_New_Demo_Run.zip differ diff --git a/local/rest_api_flint.example/docker-compose.yml b/local/rest_api_flint.example/docker-compose.yml index ef79283a..c8072d7c 100644 --- a/local/rest_api_flint.example/docker-compose.yml +++ b/local/rest_api_flint.example/docker-compose.yml @@ -1,7 +1,11 @@ version: "3.9" services: flint.example: - build: . + # - when developing use local builds + # build: . + # - or deploy using our CI + image: ghcr.io/moja-global/rest_api_flint.example:master + container_name: flint.example ports: - "8080:8080" volumes: diff --git a/local/rest_api_gcbm/app.py b/local/rest_api_gcbm/app.py index a52fb5cd..ce5a3b3c 100644 --- a/local/rest_api_gcbm/app.py +++ b/local/rest_api_gcbm/app.py @@ -147,9 +147,9 @@ def gcbm_new(): title = request.form.get("title") or "simulation" # Sanitize title title = "".join(c for c in title if c.isalnum()) - project_dir = f"{title}" - if not os.path.exists(f"{os.getcwd()}/input/{project_dir}"): - os.makedirs(f"{os.getcwd()}/input/{project_dir}") + input_dir = f"{title}" + if not os.path.exists(f"{input_dir}"): + os.makedirs(f"{input_dir}") message = "New simulation started. Please move on to the next stage for uploading files at /gcbm/upload." else: message = "Simulation already exists. Please check the list of simulations present before proceeding with a new simulation at gcbm/list. You may also download the input and output files for this simulation at gcbm/download sending parameter title in the body." @@ -181,105 +181,110 @@ def gcbm_upload(): title = "".join(c for c in title if c.isalnum()) # Create project directory - project_dir = f"{title}" - if not os.path.exists(f"{os.getcwd()}/input/{project_dir}"): - os.makedirs(f"{os.getcwd()}/input/{project_dir}") + input_dir = f"{os.getcwd()}/input/{title}" + if not os.path.exists(f"{input_dir}"): + os.makedirs(f"{input_dir}") logging.debug(os.getcwd()) - if os.path.exists(f"{os.getcwd()}/input/{project_dir}") and not os.path.exists(f"{os.getcwd()}/input/{project_dir}/disturbances"): - os.makedirs(f"{os.getcwd()}/input/{project_dir}/disturbances") - if os.path.exists(f"{os.getcwd()}/input/{project_dir}") and not os.path.exists(f"{os.getcwd()}/input/{project_dir}/classifiers"): - os.makedirs(f"{os.getcwd()}/input/{project_dir}/classifiers") - if os.path.exists(f"{os.getcwd()}/input/{project_dir}") and not os.path.exists(f"{os.getcwd()}/input/{project_dir}/db"): - os.makedirs(f"{os.getcwd()}/input/{project_dir}/db") - if os.path.exists(f"{os.getcwd()}/input/{project_dir}") and not os.path.exists(f"{os.getcwd()}/input/{project_dir}/miscellaneous"): - os.makedirs(f"{os.getcwd()}/input/{project_dir}/miscellaneous") - if os.path.exists(f"{os.getcwd()}/input/{project_dir}") and not os.path.exists(f"{os.getcwd()}/input/{project_dir}/templates"): - os.makedirs(f"{os.getcwd()}/input/{project_dir}/templates") - - # Function to flatten paths - def fix_path(path): - return os.path.basename(path.replace("\\", "/")) - + # input files follow a strict structure + if not os.path.exists(f"{input_dir}/disturbances"): + os.makedirs(f"{input_dir}/disturbances") + if not os.path.exists(f"{input_dir}/classifiers"): + os.makedirs(f"{input_dir}/classifiers") + if not os.path.exists(f"{input_dir}/db"): + os.makedirs(f"{input_dir}/db") + if not os.path.exists(f"{input_dir}/miscellaneous"): + os.makedirs(f"{input_dir}/miscellaneous") + + + # store files following structure defined in curl.md if "disturbances" in request.files: for file in request.files.getlist("disturbances"): - file.save(f"{os.getcwd()}/input/{project_dir}/disturbances/{file.filename}") + file.save(f"{input_dir}/disturbances/{file.filename}") else: return{"error": "Missing configuration file"}, 400 if "classifiers" in request.files: for file in request.files.getlist("classifiers"): - file.save(f"{os.getcwd()}/input/{project_dir}/classifiers/{file.filename}") + file.save(f"{input_dir}/classifiers/{file.filename}") else: return{"error": "Missing configuration file"}, 400 if "db" in request.files: for file in request.files.getlist("db"): - file.save(f"{os.getcwd()}/input/{project_dir}/db/{file.filename}") + file.save(f"{input_dir}/db/{file.filename}") else: return{"error": "Missing configuration file"}, 400 if "miscellaneous" in request.files: for file in request.files.getlist("miscellaneous"): - file.save(f"{os.getcwd()}/input/{project_dir}/miscellaneous/{file.filename}") - else: - return{"error": "Missing configuration file"}, 400 - - if "templates" in request.files: - for file in request.files.getlist("templates"): - file.save(f"{os.getcwd()}/input/{project_dir}/templates/{file.filename}") + file.save(f"{input_dir}/miscellaneous/{file.filename}") else: return{"error": "Missing configuration file"}, 400 - - get_modules_cbm(project_dir) - get_provider_config(project_dir) - # layers(project_dir) + get_config_templates(input_dir) + get_modules_cbm_config(input_dir) + get_provider_config(input_dir) return { "data": "All files uploaded succesfully. Proceed to the next step of the API at gcbm/dynamic." } -def get_modules_cbm(project_dir): - with open(f"{os.getcwd()}/input/{project_dir}/templates/modules_cbm.json", "r+") as pcf: +def get_config_templates(input_dir): + if not os.path.exists(f"{input_dir}/templates"): + shutil.copytree(f"{os.getcwd()}/templates", f"{input_dir}/templates", dirs_exist_ok=False) + + +# TODO: there needs to be a link between the files configured here append +# the ["vars"] attribute of modules_cbm.json -> CBMDisturbanceListener +# current hack is to drop the last five characters, but thats very fragile +def get_modules_cbm_config(input_dir): + with open(f"{input_dir}/templates/modules_cbm.json", "r+") as pcf: disturbances = [] data = json.load(pcf) - for file in os.listdir(f"{os.getcwd()}/input/{project_dir}/disturbances/"): - disturbances.append(file.split('.')[0]) + for file in os.listdir(f"{input_dir}/disturbances/"): + disturbances.append(file.split('.')[0][:-5]) # drop `_moja` to match modules_cbm.json template pcf.seek(0) data["Modules"]["CBMDisturbanceListener"]["settings"]["vars"] = disturbances json.dump(data, pcf, indent=4) pcf.truncate() -def get_provider_config(project_dir): - with open(f"{os.getcwd()}/input/{project_dir}/templates/provider_config.json", "r+") as gpc: +def get_provider_config(input_dir): + with open(f"{input_dir}/templates/provider_config.json", "r+") as gpc: # why gpc? lst = [] data = json.load(gpc) - - for file in os.listdir(f"{os.getcwd()}/input/{project_dir}/disturbances/"): + + for file in os.listdir(f"{input_dir}/db/"): + d = dict() + d["path"] = file + d["type"] = "SQLite" + data["Providers"]["SQLite"] = d + gpc.seek(0) + + for file in os.listdir(f"{input_dir}/disturbances/"): d = dict() d["name"] = file[:-10] - d["layer_path"] = "../layers/tiles" + file + d["layer_path"] = file d["layer_prefix"] = file[:-5] - lst.append(d) + lst.append(d) gpc.seek(0) data["Providers"]["RasterTiled"]["layers"] = lst - for file in os.listdir(f"{os.getcwd()}/input/{project_dir}/classifiers/"): + for file in os.listdir(f"{input_dir}/classifiers/"): d = dict() d["name"] = file[:-10] - d["layer_path"] = "../layers/tiles" + file + d["layer_path"] = file d["layer_prefix"] = file[:-5] - lst.append(d) + lst.append(d) gpc.seek(0) data["Providers"]["RasterTiled"]["layers"] = lst - for file in os.listdir(f"{os.getcwd()}/input/{project_dir}/miscellaneous/"): + for file in os.listdir(f"{input_dir}/miscellaneous/"): d = dict() d["name"] = file[:-10] - d["layer_path"] = "../layers/tiles" + file + d["layer_path"] = file d["layer_prefix"] = file[:-5] - lst.append(d) + lst.append(d) gpc.seek(0) data["Providers"]["RasterTiled"]["layers"] = lst @@ -291,13 +296,13 @@ def get_provider_config(project_dir): cellLonSize = [] paths = [] - for root, dirs, files in os.walk(os.path.abspath(f"{os.getcwd()}/input/{project_dir}/disturbances/")): + for root, dirs, files in os.walk(os.path.abspath(f"{input_dir}/disturbances/")): for file in files: fp = os.path.join(root, file) Rasters.append(fp) paths.append(fp) - for root, dirs, files in os.walk(os.path.abspath(f"{os.getcwd()}/input/{project_dir}/classifiers/")): + for root, dirs, files in os.walk(os.path.abspath(f"{input_dir}/classifiers/")): for file in files: fp1 = os.path.join(root, file) Rasters.append(fp1) @@ -312,7 +317,7 @@ def get_provider_config(project_dir): cellLatSize.append(x) cellLonSize.append(y) nodata.append(n) - + result = all(element == cellLatSize[0] for element in cellLatSize) if(result): cellLat = x @@ -324,7 +329,7 @@ def get_provider_config(project_dir): tileLon = y*4000 else: print("Corrupt files") - + gpc.seek(0) data["Providers"]["RasterTiled"]["cellLonSize"] = cellLon @@ -332,7 +337,7 @@ def get_provider_config(project_dir): data["Providers"]["RasterTiled"]["blockLonSize"] = blockLon data["Providers"]["RasterTiled"]["blockLatSize"] = blockLat data["Providers"]["RasterTiled"]["tileLatSize"] = tileLat - data["Providers"]["RasterTiled"]["LonSize"] = tileLon + data["Providers"]["RasterTiled"]["tileLonSize"] = tileLon json.dump(data, gpc, indent=4) gpc.truncate() @@ -349,7 +354,10 @@ def get_provider_config(project_dir): "cellLonSize": cellLon, } - for root, dirs, files in os.walk(os.path.abspath(f"{os.getcwd()}/input/{project_dir}/miscellaneous/")): + # TODO: refactor into get_input_layer_config + # should be able to accept variable number of inputs, but requires + # means for user to specify/verify correct ["attributes"] + for root, dirs, files in os.walk(os.path.abspath(f"{input_dir}/miscellaneous/")): for file in files: fp2 = os.path.join(root, file) Rastersm.append(fp2) @@ -359,114 +367,120 @@ def get_provider_config(project_dir): d = img.nodata nodatam.append(d) - - with open(f"{os.getcwd()}/input/{project_dir}/miscellaneous/intial_age_moja.json", 'w', encoding ='utf8') as json_file1: + with open(f"{input_dir}/initial_age_moja.json", 'w', encoding ='utf8') as json_file1: dictionary["layer_type"] = "GridLayer" dictionary["layer_data"] = "Int16" dictionary["nodata"] = nodatam[1] json.dump(dictionary, json_file1, indent = 4) - with open(f"{os.getcwd()}/input/{project_dir}/miscellaneous/mean_annual_temperature_moja.json", 'w', encoding ='utf8') as json_file2: + with open(f"{input_dir}/mean_annual_temperature_moja.json", 'w', encoding ='utf8') as json_file2: dictionary["layer_type"] = "GridLayer" dictionary["layer_data"] = "Float32" dictionary["nodata"] = nodatam[0] json.dump(dictionary, json_file2, indent = 4) - with open(f"{os.getcwd()}/input/{project_dir}/classifiers/Classifier1_moja.json", 'w', encoding ='utf8') as json_file3: - dictionary["attributes"] = {"1": "TA", "2": "BP", "3": "BS", "4": "JP", "5": "WS", "6": "WB", "7": "BF", "8": "GA"} + with open(f"{input_dir}/Classifier1_moja.json", 'w', encoding ='utf8') as json_file3: + dictionary["layer_type"] = "GridLayer" + dictionary["layer_data"] = "Byte" + dictionary["nodata"] = nd + dictionary["attributes"] = {"1": "TA", "2": "BP", "3": "BS", "4": "JP", "5": "WS", "6": "WB", "7": "BF", "8": "GA"} json.dump(dictionary, json_file3, indent = 4) - with open(f"{os.getcwd()}/input/{project_dir}/classifiers/Classifier2_moja.json", 'w', encoding ='utf8') as json_file4: + with open(f"{input_dir}/Classifier2_moja.json", 'w', encoding ='utf8') as json_file4: + dictionary["layer_type"] = "GridLayer" + dictionary["layer_data"] = "Byte" + dictionary["nodata"] = nd dictionary["attributes"] = {"1": "5", "2": "6","3": "7", - "4": "8"} + "4": "8"} json.dump(dictionary, json_file4, indent = 4) - with open(f"{os.getcwd()}/input/{project_dir}/disturbances/disturbances_2011_moja.json", 'w', encoding ='utf8') as json_file5: + with open(f"{input_dir}/disturbances_2011_moja.json", 'w', encoding ='utf8') as json_file5: dictionary["layer_data"] = "Byte" dictionary["nodata"] = nd dictionary["attributes"] = { - "1": { - "year": 2011, - "disturbance_type": "Wildfire", - "transition": 1 - } - } + "1": { + "year": 2011, + "disturbance_type": "Wildfire", + "transition": 1 + } + } json.dump(dictionary, json_file5, indent = 4) - with open(f"{os.getcwd()}/input/{project_dir}/disturbances/disturbances_2012_moja.json", 'w', encoding ='utf8') as json_file6: + with open(f"{input_dir}/disturbances_2012_moja.json", 'w', encoding ='utf8') as json_file6: dictionary["attributes"] = { - "1": { - "year": 2012, - "disturbance_type": "Wildfire", - "transition": 1 - } - } + "1": { + "year": 2012, + "disturbance_type": "Wildfire", + "transition": 1 + } + } json.dump(dictionary, json_file6, indent = 4) - with open(f"{os.getcwd()}/input/{project_dir}/disturbances/disturbances_2013_moja.json", 'w', encoding ='utf8') as json_file7: + with open(f"{input_dir}/disturbances_2013_moja.json", 'w', encoding ='utf8') as json_file7: dictionary["attributes"] = { - "1": { - "year": 2013, - "disturbance_type": "Mountain pine beetle — Very severe impact", - "transition": 1 - }, - "2": { - "year": 2013, - "disturbance_type": "Wildfire", - "transition": 1 - } - } + "1": { + "year": 2013, + "disturbance_type": "Mountain pine beetle — Very severe impact", + "transition": 1 + }, + "2": { + "year": 2013, + "disturbance_type": "Wildfire", + "transition": 1 + } + } json.dump(dictionary, json_file7, indent = 4) - with open(f"{os.getcwd()}/input/{project_dir}/disturbances/disturbances_2014_moja.json", 'w', encoding ='utf8') as json_file8: + with open(f"{input_dir}/disturbances_2014_moja.json", 'w', encoding ='utf8') as json_file8: dictionary["attributes"] = { - "1": { - "year": 2014, - "disturbance_type": "Mountain pine beetle — Very severe impact", - "transition": 1 - } - } + "1": { + "year": 2014, + "disturbance_type": "Mountain pine beetle — Very severe impact", + "transition": 1 + } + } json.dump(dictionary, json_file8, indent = 4) - with open(f"{os.getcwd()}/input/{project_dir}/disturbances/disturbances_2015_moja.json", 'w', encoding ='utf8') as json_file9: + with open(f"{input_dir}/disturbances_2015_moja.json", 'w', encoding ='utf8') as json_file9: dictionary["attributes"] = { - "1": { - "year": 2016, - "disturbance_type": "Wildfire", - "transition": 1 - } - } + "1": { + "year": 2016, + "disturbance_type": "Wildfire", + "transition": 1 + } + } json.dump(dictionary, json_file9, indent = 4) - with open(f"{os.getcwd()}/input/{project_dir}/disturbances/disturbances_2016_moja.json", 'w', encoding ='utf8') as json_file10: + with open(f"{input_dir}/disturbances_2016_moja.json", 'w', encoding ='utf8') as json_file10: dictionary["attributes"] = { - "1": { - "year": 2016, - "disturbance_type": "Wildfire", - "transition": 1 - } - } + "1": { + "year": 2016, + "disturbance_type": "Wildfire", + "transition": 1 + } + } json.dump(dictionary, json_file10, indent = 4) - with open(f"{os.getcwd()}/input/{project_dir}/disturbances/disturbances_2018_moja.json", 'w', encoding ='utf8') as json_file11: + with open(f"{input_dir}/disturbances_2018_moja.json", 'w', encoding ='utf8') as json_file11: dictionary["attributes"] = { - "1": { - "year": 2018, - "disturbance_type": "Mountain pine beetle — Low impact", - "transition": 1 - } - } + "1": { + "year": 2018, + "disturbance_type": "Mountain pine beetle — Low impact", + "transition": 1 + } + } json.dump(dictionary, json_file11, indent = 4) + # TODO: Refactor into get_study_area_config study_area = { "tile_size": tileLat, "block_size": blockLat, "tiles": [{ - "x": t[2], - "y": t[5], - "index": 12674 + "x": int(t[2]), # FLINT is strongly typed, but maybe it doesnt matter, just following format in GCBM_Demo_Run + "y": int(t[5]), + "index": 12674 # verify source of this number }], "pixel_size": cellLat, "layers": [ @@ -474,18 +488,17 @@ def get_provider_config(project_dir): ] } - with open(f"{os.getcwd()}/input/{project_dir}/miscellaneous/study_area.json", 'w', encoding = 'utf') as json_file: - + with open(f"{input_dir}/study_area.json", 'w', encoding = 'utf') as json_file: list = [] - for file in os.listdir(f"{os.getcwd()}/input/{project_dir}/miscellaneous/"): + for file in os.listdir(f"{input_dir}/miscellaneous/"): d1 = dict() d1["name"] = file[:-10] d1["type"] = "VectorLayer" list.append(d1) study_area["layers"] = list - for file in os.listdir(f"{os.getcwd()}/input/{project_dir}/classifiers/"): + for file in os.listdir(f"{input_dir}/classifiers/"): d1 = dict() d1["name"] = file[:-10] d1["type"] = "VectorLayer" @@ -493,7 +506,7 @@ def get_provider_config(project_dir): list.append(d1) study_area["layers"] = list - for file in os.listdir(f"{os.getcwd()}/input/{project_dir}/disturbances/"): + for file in os.listdir(f"{input_dir}/disturbances/"): d1 = dict() d1["name"] = file[:-10] d1["type"] = "DisturbanceLayer" @@ -501,47 +514,45 @@ def get_provider_config(project_dir): list.append(d1) study_area["layers"] = list - - json.dump(study_area, json_file, indent=4) - for root, dirs, files in os.walk(os.path.abspath(f"{os.getcwd()}/input/{project_dir}/disturbances/")): + for root, dirs, files in os.walk(os.path.abspath(f"{input_dir}/disturbances/")): for file in files: fp = os.path.join(root, file) Rasters.append(fp) paths.append(fp) - for root, dirs, files in os.walk(os.path.abspath(f"{os.getcwd()}/input/{project_dir}/classifiers/")): + for root, dirs, files in os.walk(os.path.abspath(f"{input_dir}/classifiers/")): for file in files: fp1 = os.path.join(root, file) Rasters.append(fp1) paths.append(fp1) - for root, dirs, files in os.walk(os.path.abspath(f"{os.getcwd()}/input/{project_dir}/miscellaneous/")): + for root, dirs, files in os.walk(os.path.abspath(f"{input_dir}/miscellaneous/")): for file in files: fp2 = os.path.join(root, file) paths.append(fp2) - for root, dirs, files in os.walk(os.path.abspath(f"{os.getcwd()}/input/{project_dir}/templates/")): + for root, dirs, files in os.walk(os.path.abspath(f"{input_dir}/templates/")): for file in files: fp3 = os.path.join(root, file) paths.append(fp3) - for root, dirs, files in os.walk(os.path.abspath(f"{os.getcwd()}/input/{project_dir}/db/")): + for root, dirs, files in os.walk(os.path.abspath(f"{input_dir}/db/")): for file in files: fp4 = os.path.join(root, file) paths.append(fp4) for i in paths: print(i) - shutil.copy2(i, (f"{os.getcwd()}/input/{project_dir}")) + shutil.copy2(i, (f"{input_dir}")) - shutil.rmtree((f"{os.getcwd()}/input/{project_dir}/disturbances/")) - shutil.rmtree((f"{os.getcwd()}/input/{project_dir}/templates/")) - shutil.rmtree((f"{os.getcwd()}/input/{project_dir}/classifiers/")) - shutil.rmtree((f"{os.getcwd()}/input/{project_dir}/miscellaneous/")) - shutil.rmtree((f"{os.getcwd()}/input/{project_dir}/db/")) + shutil.rmtree((f"{input_dir}/disturbances/")) + shutil.rmtree((f"{input_dir}/templates/")) + shutil.rmtree((f"{input_dir}/classifiers/")) + shutil.rmtree((f"{input_dir}/miscellaneous/")) + shutil.rmtree((f"{input_dir}/db/")) @app.route("/gcbm/dynamic", methods=["POST"]) def gcbm_dynamic(): @@ -562,28 +573,29 @@ def gcbm_dynamic(): """ # Default title = simulation title = request.form.get("title") or "simulation" + # Sanitize title title = "".join(c for c in title if c.isalnum()) - project_dir = f"{title}" + input_dir = f"{os.getcwd()}/input/{title}" gcbm_config_path = "gcbm_config.cfg" provider_config_path = "provider_config.json" - if not os.path.exists(f"{os.getcwd()}/input/{project_dir}"): - os.makedirs(f"{os.getcwd()}/input/{project_dir}") + if not os.path.exists(f"{input_dir}"): + os.makedirs(f"{input_dir}") thread = Thread( - target=launch_run, kwargs={"title": title, "project_dir": project_dir} + target=launch_run, kwargs={"title": title, "input_dir": input_dir} ) thread.start() # subscriber_path = create_topic_and_sub(title) return {"status": "Run started"}, 200 -def launch_run(title, project_dir): +def launch_run(title, input_dir): s = time.time() logging.debug("Starting run") - with open(f"{os.getcwd()}/input/{project_dir}/gcbm_logs.csv", "w+") as f: + with open(f"{input_dir}/gcbm_logs.csv", "w+") as f: res = subprocess.Popen( [ "/opt/gcbm/moja.cli", @@ -593,20 +605,23 @@ def launch_run(title, project_dir): "provider_config.json", ], stdout=f, - cwd=f"{os.getcwd()}/input/{project_dir}", + cwd=f"{input_dir}", ) logging.debug("Communicating") (output, err) = res.communicate() logging.debug("Communicated") - if not os.path.exists(f"{os.getcwd()}/input/{project_dir}/output"): + + # TODO: this should go in `output/title/` but will need updating in + # get_modules_cbm_config and download() + if not os.path.exists(f"{input_dir}/output"): logging.error(err) return "OK" logging.debug("Output exists") - # returncode = final_run(title, gcbm_config_path, provider_config_path, project_dir) + # returncode = final_run(title, gcbm_config_path, provider_config_path, input_dir) shutil.make_archive( - f"{os.getcwd()}/input/{project_dir}/output", + f"{input_dir}/output", "zip", - f"{os.getcwd()}/input/{project_dir}/output", + f"{input_dir}/output", ) logging.debug("Made archive") e = time.time() @@ -640,9 +655,9 @@ def gcbm_download(): title = request.form.get("title") or "simulation" # Sanitize title title = "".join(c for c in title if c.isalnum()) - project_dir = f"{title}" + input_dir = f"{title}" return send_file( - f"{os.getcwd()}/input/{project_dir}/output.zip", + f"{input_dir}/output.zip", attachment_filename="output.zip", ) diff --git a/local/rest_api_gcbm/curl.md b/local/rest_api_gcbm/curl.md index 5737d90a..2565fa41 100644 --- a/local/rest_api_gcbm/curl.md +++ b/local/rest_api_gcbm/curl.md @@ -1,6 +1,6 @@

Endpoints

-1. `/gcbm/new/` +1. `/gcbm/new/` The title of a new simulation can be passed or the default title `simulation` will be used. (i.e the default title of the simulation is `simulation`, here the title `run4` is used) @@ -31,15 +31,6 @@ -F db='@db/gcbm_input.db' \ -F miscellaneous='@miscellaneous/initial_age_moja.tiff' \ -F miscellaneous='@miscellaneous/mean_annual_temperature_moja.tiff' \ - -F templates='@templates/internal_variables.json' \ - -F templates='@templates/localdomain.json' \ - -F templates='@templates/modules_cbm.json' \ - -F templates='@templates/modules_output.json' \ - -F templates='@templates/pools_cbm.json' \ - -F templates='@templates/provider_config.json' \ - -F templates='@templates/spinup.json' \ - -F templates='@templates/variables.json' \ - -F templates='@templates/gcbm_config.cfg' \ -F title="run4" \ http://localhost:8080/gcbm/upload @@ -56,7 +47,7 @@ ``` 5. `/gcbm/download` A file named `output.zip` will be obtained. This file contains the outputs generated, which can be analysed on unzipping. - + ``` curl -d "title=run4" -X POST http://localhost:8080/gcbm/download -L -o output.zip ``` diff --git a/local/rest_api_gcbm/docker-compose.yml b/local/rest_api_gcbm/docker-compose.yml index e11802f6..03ffdbe6 100644 --- a/local/rest_api_gcbm/docker-compose.yml +++ b/local/rest_api_gcbm/docker-compose.yml @@ -1,9 +1,12 @@ version: "3.9" services: gcbm: + # - when developing use local builds build: . + # - or deploy using our CI + # image: ghcr.io/moja-global/rest_api_gcbm:master + container_name: flint.gcbm ports: - "8080:8080" volumes: - .:/app - diff --git a/local/rest_api_gcbm/run4/gcbm_logs.csv b/local/rest_api_gcbm/run4/gcbm_logs.csv new file mode 100644 index 00000000..e69de29b diff --git a/local/rest_api_gcbm/templates/gcbm_config.cfg b/local/rest_api_gcbm/templates/gcbm_config.cfg new file mode 100644 index 00000000..e6640ac9 --- /dev/null +++ b/local/rest_api_gcbm/templates/gcbm_config.cfg @@ -0,0 +1,7 @@ +config=localdomain.json +config=pools_cbm.json +config=modules_cbm.json +config=modules_output.json +config=spinup.json +config=variables.json +config=internal_variables.json diff --git a/local/rest_api_gcbm/templates/internal_variables.json b/local/rest_api_gcbm/templates/internal_variables.json new file mode 100644 index 00000000..d9f71537 --- /dev/null +++ b/local/rest_api_gcbm/templates/internal_variables.json @@ -0,0 +1,34 @@ +{ + "Variables": { + "spatialLocationInfo": { + "flintdata": { + "type": "SpatialLocationInfo", + "library": "internal.flint", + "settings": {} + } + }, + "simulateLandUnit": true, + "is_decaying": true, + "spinup_moss_only": false, + "run_peatland": false, + "peatlandId": -1, + "is_forest": true, + "run_moss": false, + "run_delay": false, + "landUnitBuildSuccess": true, + "regen_delay": 0, + "age": 0, + "tileIndex": 0, + "blockIndex": 0, + "cellIndex": 0, + "LandUnitId": -1, + "landUnitArea": 0, + "classifier_set": {}, + "localDomainId": 0, + "LocalDomainId": 1, + "age_class": 0, + "historic_land_class": "FL", + "current_land_class": "FL", + "unfccc_land_class": "UNFCCC_FL_R_FL" + } +} \ No newline at end of file diff --git a/local/rest_api_gcbm/templates/localdomain.json b/local/rest_api_gcbm/templates/localdomain.json new file mode 100644 index 00000000..5bb04f33 --- /dev/null +++ b/local/rest_api_gcbm/templates/localdomain.json @@ -0,0 +1,31 @@ +{ + "Libraries": { + "moja.modules.cbm": "external", + "moja.modules.gdal": "external" + }, + "LocalDomain": { + "start_date": "2010/01/01", + "end_date": "2021/01/01", + "landUnitBuildSuccess": "landUnitBuildSuccess", + "simulateLandUnit": "simulateLandUnit", + "sequencer_library": "moja.modules.cbm", + "sequencer": "CBMSequencer", + "timing": "annual", + "type": "spatial_tiled", + "landscape": { + "provider": "RasterTiled", + "num_threads": 4, + "tiles": [ + { + "x": -106, + "y": 55, + "index": 12674 + } + ], + "x_pixels": 4000, + "y_pixels": 4000, + "tile_size_x": 1.0, + "tile_size_y": 1.0 + } + } +} \ No newline at end of file diff --git a/local/rest_api_gcbm/templates/logging.conf b/local/rest_api_gcbm/templates/logging.conf new file mode 100644 index 00000000..2da5fec5 --- /dev/null +++ b/local/rest_api_gcbm/templates/logging.conf @@ -0,0 +1,17 @@ +[Core] +DisableLogging=false + +[Sinks.console] +Destination=Console +Asynchronous=false +AutoFlush=true +Format="<%TimeStamp%> (%Severity%) - %Message%" +Filter="%Severity% >= info" + +[Sinks.file] +Destination=TextFile +FileName="output/simulation.log" +Asynchronous=false +AutoFlush=true +Format="<%TimeStamp%> (%Severity%) - %Message%" +Filter="%Severity% >= debug" diff --git a/local/rest_api_gcbm/templates/modules_cbm.json b/local/rest_api_gcbm/templates/modules_cbm.json new file mode 100644 index 00000000..4db5d37b --- /dev/null +++ b/local/rest_api_gcbm/templates/modules_cbm.json @@ -0,0 +1,65 @@ +{ + "Modules": { + "CBMBuildLandUnitModule": { + "order": 1, + "library": "moja.modules.cbm" + }, + "CBMSequencer": { + "order": 2, + "library": "moja.modules.cbm" + }, + "CBMDisturbanceListener": { + "enabled": true, + "order": 3, + "library": "moja.modules.cbm", + "settings": { + "vars": [ + "disturbances_2012_moja", + "disturbances_2015_moja", + "disturbances_2014_moja", + "disturbances_2018_moja", + "disturbances_2016_moja", + "disturbances_2013_moja", + "disturbances_2011_moja" + ] + } + }, + "CBMDisturbanceEventModule": { + "enabled": true, + "order": 4, + "library": "moja.modules.cbm" + }, + "CBMTransitionRulesModule": { + "enabled": true, + "order": 5, + "library": "moja.modules.cbm" + }, + "CBMLandClassTransitionModule": { + "enabled": true, + "order": 6, + "library": "moja.modules.cbm" + }, + "CBMGrowthModule": { + "enabled": true, + "order": 7, + "library": "moja.modules.cbm" + }, + "CBMDecayModule": { + "enabled": true, + "order": 8, + "library": "moja.modules.cbm", + "settings": { + "extra_decay_removals": false + } + }, + "CBMAgeIndicators": { + "enabled": true, + "order": 9, + "library": "moja.modules.cbm" + }, + "TransactionManagerAfterSubmitModule": { + "order": 10, + "library": "internal.flint" + } + } +} \ No newline at end of file diff --git a/local/rest_api_gcbm/templates/modules_output.json b/local/rest_api_gcbm/templates/modules_output.json new file mode 100644 index 00000000..cad01b28 --- /dev/null +++ b/local/rest_api_gcbm/templates/modules_output.json @@ -0,0 +1,816 @@ +{ + "Modules": { + "WriteVariableGeotiff": { + "enabled": true, + "order": 11, + "library": "moja.modules.gdal", + "settings": { + "items": [ + { + "data_name": "Age", + "enabled": true, + "variable_data_type": "Int16", + "on_notification": "OutputStep", + "variable_name": "age" + }, + { + "pool_name": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther" + ], + "data_name": "AG_Biomass_C", + "enabled": true, + "variable_data_type": "float", + "on_notification": "OutputStep" + }, + { + "pool_name": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots", + "AboveGroundVeryFastSoil", + "BelowGroundVeryFastSoil", + "AboveGroundFastSoil", + "BelowGroundFastSoil", + "MediumSoil", + "AboveGroundSlowSoil", + "BelowGroundSlowSoil", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ], + "data_name": "Total_Ecosystem_C", + "enabled": true, + "variable_data_type": "float", + "on_notification": "OutputStep" + }, + { + "pool_name": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots" + ], + "data_name": "Total_Biomass_C", + "enabled": true, + "variable_data_type": "float", + "on_notification": "OutputStep" + }, + { + "pool_name": [ + "AboveGroundVeryFastSoil", + "BelowGroundVeryFastSoil", + "AboveGroundFastSoil", + "BelowGroundFastSoil", + "MediumSoil", + "AboveGroundSlowSoil", + "BelowGroundSlowSoil", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ], + "data_name": "Dead_Organic_Matter_C", + "enabled": true, + "variable_data_type": "float", + "on_notification": "OutputStep" + }, + { + "pool_name": [ + "BelowGroundVeryFastSoil", + "BelowGroundSlowSoil" + ], + "data_name": "Soil_C", + "enabled": true, + "variable_data_type": "float", + "on_notification": "OutputStep" + }, + { + "variable_data_type": "float", + "flux": { + "to": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots" + ], + "from": [ + "Atmosphere" + ] + }, + "data_name": "NPP", + "enabled": true, + "on_notification": "OutputStep" + }, + { + "variable_data_type": "float", + "flux": [ + { + "to": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots" + ], + "from": [ + "Atmosphere" + ] + }, + { + "subtract": true, + "from": [ + "AboveGroundVeryFastSoil", + "BelowGroundVeryFastSoil", + "AboveGroundFastSoil", + "BelowGroundFastSoil", + "MediumSoil", + "AboveGroundSlowSoil", + "BelowGroundSlowSoil", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ], + "to": [ + "CO2", + "CH4", + "CO" + ], + "flux_source": "annual_process" + } + ], + "data_name": "NEP", + "enabled": true, + "on_notification": "OutputStep" + }, + { + "variable_data_type": "float", + "flux": [ + { + "flux_source": "annual_process", + "from": [ + "AboveGroundVeryFastSoil", + "BelowGroundVeryFastSoil", + "AboveGroundFastSoil", + "BelowGroundFastSoil", + "MediumSoil", + "AboveGroundSlowSoil", + "BelowGroundSlowSoil", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ], + "to": [ + "CO2", + "CH4", + "CO" + ] + } + ], + "data_name": "Decomp_Releases", + "enabled": true, + "on_notification": "OutputStep" + }, + { + "variable_data_type": "float", + "flux": [ + { + "to": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots" + ], + "from": [ + "Atmosphere" + ] + }, + { + "subtract": true, + "from": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ], + "to": [ + "Products" + ], + "flux_source": "disturbance" + }, + { + "from": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots", + "AboveGroundVeryFastSoil", + "BelowGroundVeryFastSoil", + "AboveGroundFastSoil", + "BelowGroundFastSoil", + "MediumSoil", + "AboveGroundSlowSoil", + "BelowGroundSlowSoil", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ], + "subtract": true, + "to": [ + "CO2", + "CH4", + "CO" + ] + } + ], + "data_name": "NBP", + "enabled": true, + "on_notification": "OutputStep" + }, + { + "variable_data_type": "float", + "flux": [ + { + "to": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots" + ], + "from": [ + "Atmosphere" + ] + }, + { + "from": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots", + "AboveGroundVeryFastSoil", + "BelowGroundVeryFastSoil", + "AboveGroundFastSoil", + "BelowGroundFastSoil", + "MediumSoil", + "AboveGroundSlowSoil", + "BelowGroundSlowSoil", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ], + "subtract": true, + "to": [ + "CO2", + "CH4", + "CO" + ] + }, + { + "subtract": true, + "from": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ], + "to": [ + "Products" + ], + "flux_source": "disturbance" + } + ], + "data_name": "Delta_Total_Ecosystem", + "enabled": true, + "on_notification": "OutputStep" + }, + { + "variable_data_type": "float", + "flux": [ + { + "to": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots" + ], + "from": [ + "Atmosphere" + ] + }, + { + "from": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots" + ], + "subtract": true, + "to": [ + "CO2", + "CH4", + "CO" + ] + }, + { + "subtract": true, + "from": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots" + ], + "to": [ + "Products" + ], + "flux_source": "disturbance" + }, + { + "from": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots" + ], + "subtract": true, + "to": [ + "AboveGroundVeryFastSoil", + "BelowGroundVeryFastSoil", + "AboveGroundFastSoil", + "BelowGroundFastSoil", + "MediumSoil", + "AboveGroundSlowSoil", + "BelowGroundSlowSoil", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ] + } + ], + "data_name": "Delta_Total_Biomass", + "enabled": true, + "on_notification": "OutputStep" + }, + { + "variable_data_type": "float", + "flux": [ + { + "from": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots" + ], + "to": [ + "AboveGroundVeryFastSoil", + "BelowGroundVeryFastSoil", + "AboveGroundFastSoil", + "BelowGroundFastSoil", + "MediumSoil", + "AboveGroundSlowSoil", + "BelowGroundSlowSoil", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ] + }, + { + "from": [ + "AboveGroundVeryFastSoil", + "BelowGroundVeryFastSoil", + "AboveGroundFastSoil", + "BelowGroundFastSoil", + "MediumSoil", + "AboveGroundSlowSoil", + "BelowGroundSlowSoil", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ], + "subtract": true, + "to": [ + "CO2", + "CH4", + "CO", + "Products" + ] + } + ], + "data_name": "Delta_Total_DOM", + "enabled": true, + "on_notification": "OutputStep" + }, + { + "variable_data_type": "float", + "flux": { + "to": [ + "CO2", + "CH4", + "CO" + ], + "from": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots", + "AboveGroundVeryFastSoil", + "BelowGroundVeryFastSoil", + "AboveGroundFastSoil", + "BelowGroundFastSoil", + "MediumSoil", + "AboveGroundSlowSoil", + "BelowGroundSlowSoil", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ] + }, + "data_name": "Total_Emissions", + "enabled": true, + "on_notification": "OutputStep" + }, + { + "variable_data_type": "float", + "flux": { + "to": [ + "CO2", + "CH4", + "CO" + ], + "from": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots" + ] + }, + "data_name": "Total_Biomass_Emissions", + "enabled": true, + "on_notification": "OutputStep" + }, + { + "variable_data_type": "float", + "flux": { + "to": [ + "CO2", + "CH4", + "CO" + ], + "from": [ + "AboveGroundVeryFastSoil", + "BelowGroundVeryFastSoil", + "AboveGroundFastSoil", + "BelowGroundFastSoil", + "MediumSoil", + "AboveGroundSlowSoil", + "BelowGroundSlowSoil", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ] + }, + "data_name": "Total_DOM_Emissions", + "enabled": true, + "on_notification": "OutputStep" + }, + { + "variable_data_type": "float", + "flux": { + "to": [ + "CO2" + ], + "from": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots", + "AboveGroundVeryFastSoil", + "BelowGroundVeryFastSoil", + "AboveGroundFastSoil", + "BelowGroundFastSoil", + "MediumSoil", + "AboveGroundSlowSoil", + "BelowGroundSlowSoil", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ] + }, + "data_name": "Total_CO2_Emissions", + "enabled": true, + "on_notification": "OutputStep" + }, + { + "variable_data_type": "float", + "flux": { + "to": [ + "CO" + ], + "from": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots", + "AboveGroundVeryFastSoil", + "BelowGroundVeryFastSoil", + "AboveGroundFastSoil", + "BelowGroundFastSoil", + "MediumSoil", + "AboveGroundSlowSoil", + "BelowGroundSlowSoil", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ] + }, + "data_name": "Total_CO_Emissions", + "enabled": true, + "on_notification": "OutputStep" + }, + { + "variable_data_type": "float", + "flux": { + "to": [ + "CH4" + ], + "from": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots", + "AboveGroundVeryFastSoil", + "BelowGroundVeryFastSoil", + "AboveGroundFastSoil", + "BelowGroundFastSoil", + "MediumSoil", + "AboveGroundSlowSoil", + "BelowGroundSlowSoil", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ] + }, + "data_name": "Total_CH4_Emissions", + "enabled": true, + "on_notification": "OutputStep" + }, + { + "variable_data_type": "float", + "flux": [ + { + "to": [ + "CO2", + "CH4", + "CO" + ], + "from": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots", + "AboveGroundVeryFastSoil", + "BelowGroundVeryFastSoil", + "AboveGroundFastSoil", + "BelowGroundFastSoil", + "MediumSoil", + "AboveGroundSlowSoil", + "BelowGroundSlowSoil", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ] + }, + { + "flux_source": "disturbance", + "from": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ], + "to": [ + "Products" + ] + } + ], + "data_name": "Ecosystem_Removals", + "enabled": true, + "on_notification": "OutputStep" + }, + { + "variable_data_type": "float", + "flux": { + "flux_source": "disturbance", + "from": [ + "SoftwoodMerch", + "SoftwoodFoliage", + "SoftwoodOther", + "SoftwoodCoarseRoots", + "SoftwoodFineRoots", + "HardwoodMerch", + "HardwoodFoliage", + "HardwoodOther", + "HardwoodCoarseRoots", + "HardwoodFineRoots" + ], + "to": [ + "AboveGroundVeryFastSoil", + "BelowGroundVeryFastSoil", + "AboveGroundFastSoil", + "BelowGroundFastSoil", + "MediumSoil", + "AboveGroundSlowSoil", + "BelowGroundSlowSoil", + "SoftwoodStemSnag", + "SoftwoodBranchSnag", + "HardwoodStemSnag", + "HardwoodBranchSnag" + ] + }, + "data_name": "Bio_To_DOM_From_Disturbances", + "enabled": true, + "on_notification": "OutputStep" + } + ], + "output_path": "output" + } + }, + "CBMAggregatorLandUnitData": { + "enabled": true, + "order": 12, + "library": "moja.modules.cbm", + "settings": { + "reporting_classifier_set": "reporting_classifiers" + } + }, + "CBMAggregatorSQLiteWriter": { + "enabled": true, + "order": 13, + "library": "moja.modules.cbm", + "settings": { + "databasename": "output/simulation_output.db" + } + } + } +} diff --git a/local/rest_api_gcbm/templates/pools_cbm.json b/local/rest_api_gcbm/templates/pools_cbm.json new file mode 100644 index 00000000..35979d38 --- /dev/null +++ b/local/rest_api_gcbm/templates/pools_cbm.json @@ -0,0 +1,34 @@ +{ + "Pools": { + "AboveGroundFastSoil": 0.0, + "AboveGroundSlowSoil": 0.0, + "AboveGroundVeryFastSoil": 0.0, + "Atmosphere": 0.0, + "BelowGroundFastSoil": 0.0, + "BelowGroundSlowSoil": 0.0, + "BelowGroundVeryFastSoil": 0.0, + "BlackCarbon": 0.0, + "CH4": 0.0, + "CO": 0.0, + "CO2": 0.0, + "DissolvedOrganicCarbon": 0.0, + "HardwoodBranchSnag": 0.0, + "HardwoodCoarseRoots": 0.0, + "HardwoodFineRoots": 0.0, + "HardwoodFoliage": 0.0, + "HardwoodMerch": 0.0, + "HardwoodOther": 0.0, + "HardwoodStemSnag": 0.0, + "MediumSoil": 0.0, + "NO2": 0.0, + "Peat": 0.0, + "Products": 0.0, + "SoftwoodBranchSnag": 0.0, + "SoftwoodCoarseRoots": 0.0, + "SoftwoodFineRoots": 0.0, + "SoftwoodFoliage": 0.0, + "SoftwoodMerch": 0.0, + "SoftwoodOther": 0.0, + "SoftwoodStemSnag": 0.0 + } +} \ No newline at end of file diff --git a/local/rest_api_gcbm/templates/provider_config.json b/local/rest_api_gcbm/templates/provider_config.json new file mode 100644 index 00000000..1d2a9987 --- /dev/null +++ b/local/rest_api_gcbm/templates/provider_config.json @@ -0,0 +1,75 @@ +{ + "Providers": { + "SQLite": { + "path": "../input_database/gcbm_input.db", + "type": "SQLite" + }, + "RasterTiled": { + "layers": [ + { + "name": "disturbances_2012", + "layer_path": "../layers/tilesdisturbances_2012_moja.tiff", + "layer_prefix": "disturbances_2012_moja" + }, + { + "name": "disturbances_2015", + "layer_path": "../layers/tilesdisturbances_2015_moja.tiff", + "layer_prefix": "disturbances_2015_moja" + }, + { + "name": "disturbances_2014", + "layer_path": "../layers/tilesdisturbances_2014_moja.tiff", + "layer_prefix": "disturbances_2014_moja" + }, + { + "name": "disturbances_2018", + "layer_path": "../layers/tilesdisturbances_2018_moja.tiff", + "layer_prefix": "disturbances_2018_moja" + }, + { + "name": "disturbances_2016", + "layer_path": "../layers/tilesdisturbances_2016_moja.tiff", + "layer_prefix": "disturbances_2016_moja" + }, + { + "name": "disturbances_2013", + "layer_path": "../layers/tilesdisturbances_2013_moja.tiff", + "layer_prefix": "disturbances_2013_moja" + }, + { + "name": "disturbances_2011", + "layer_path": "../layers/tilesdisturbances_2011_moja.tiff", + "layer_prefix": "disturbances_2011_moja" + }, + { + "name": "Classifier1", + "layer_path": "../layers/tilesClassifier1_moja.tiff", + "layer_prefix": "Classifier1_moja" + }, + { + "name": "Classifier2", + "layer_path": "../layers/tilesClassifier2_moja.tiff", + "layer_prefix": "Classifier2_moja" + }, + { + "name": "mean_annual_temperature", + "layer_path": "../layers/tilesmean_annual_temperature_moja.tiff", + "layer_prefix": "mean_annual_temperature_moja" + }, + { + "name": "initial_age", + "layer_path": "../layers/tilesinitial_age_moja.tiff", + "layer_prefix": "initial_age_moja" + } + ], + "blockLonSize": 0.1, + "tileLatSize": 1.0, + "tileLonSize": 1.0, + "cellLatSize": 0.00025, + "cellLonSize": 0.00025, + "blockLatSize": 0.1, + "type": "RasterTiledGDAL", + "library": "moja.modules.gdal" + } + } +} diff --git a/local/rest_api_gcbm/templates/spinup.json b/local/rest_api_gcbm/templates/spinup.json new file mode 100644 index 00000000..1365b141 --- /dev/null +++ b/local/rest_api_gcbm/templates/spinup.json @@ -0,0 +1,56 @@ +{ + "Spinup": { + "enabled": true, + "sequencer_library": "moja.modules.cbm", + "simulateLandUnit": "simulateLandUnit", + "landUnitBuildSuccess": "landUnitBuildSuccess", + "sequencer": "CBMSpinupSequencer" + }, + "SpinupVariables": { + "delay": 0, + "minimum_rotation": 10, + "run_delay": false + }, + "Variables": { + "spinup_parameters": { + "transform": { + "queryString": "SELECT s.return_interval AS return_interval, s.max_rotations AS max_rotations, dt.name AS historic_disturbance_type, dt.name AS last_pass_disturbance_type, s.mean_annual_temperature AS mean_annual_temperature, 0 as delay FROM spinup_parameter s INNER JOIN disturbance_type dt ON s.historic_disturbance_type_id = dt.id INNER JOIN spatial_unit spu ON spu.spinup_parameter_id = s.id INNER JOIN admin_boundary a ON spu.admin_boundary_id = a.id INNER JOIN eco_boundary e ON spu.eco_boundary_id = e.id WHERE a.name = {var:admin_boundary} AND e.name = {var:eco_boundary}", + "type": "SQLQueryTransform", + "library": "internal.flint", + "provider": "SQLite" + } + } + }, + "SpinupModules": { + "CBMSpinupSequencer": { + "create_new": true, + "library": "moja.modules.cbm", + "order": 1 + }, + "CBMBuildLandUnitModule": { + "create_new": true, + "library": "moja.modules.cbm", + "order": 2 + }, + "CBMGrowthModule": { + "create_new": true, + "library": "moja.modules.cbm", + "order": 3 + }, + "CBMDecayModule": { + "create_new": true, + "library": "moja.modules.cbm", + "order": 4 + }, + "TransactionManagerAfterSubmitModule": { + "create_new": true, + "library": "internal.flint", + "order": 5 + }, + "CBMSpinupDisturbanceModule": { + "create_new": true, + "library": "moja.modules.cbm", + "order": 6 + } + } +} \ No newline at end of file diff --git a/local/rest_api_gcbm/templates/variables.json b/local/rest_api_gcbm/templates/variables.json new file mode 100644 index 00000000..f2d8bc2f --- /dev/null +++ b/local/rest_api_gcbm/templates/variables.json @@ -0,0 +1,257 @@ +{ + "Variables": { + "enable_peatland": false, + "enable_moss": false, + "admin_boundary": "British Columbia", + "eco_boundary": "Taiga Plains", + "initial_age": { + "transform": { + "library": "internal.flint", + "type": "LocationIdxFromFlintDataTransform", + "provider": "RasterTiled", + "data_id": "initial_age" + } + }, + "initial_historic_land_class": "FL", + "initial_current_land_class": "FL", + "age_class_range": 20, + "age_maximum": 300, + "slow_ag_to_bg_mixing_rate": 0.006, + "disturbance_matrices": { + "transform": { + "queryString": "SELECT dm.id AS disturbance_matrix_id, source_pool.name as source_pool_name, dest_pool.name as dest_pool_name, dv.proportion FROM disturbance_matrix dm INNER JOIN disturbance_matrix_value dv ON dm.id = dv.disturbance_matrix_id INNER JOIN pool source_pool ON dv.source_pool_id = source_pool.id INNER JOIN pool dest_pool ON dv.sink_pool_id = dest_pool.id", + "type": "SQLQueryTransform", + "library": "internal.flint", + "provider": "SQLite" + } + }, + "softwood_yield_table": { + "transform": { + "queryString": "SELECT gcv.age AS age, SUM(gcv.merchantable_volume) AS merchantable_volume FROM (SELECT CASE WHEN gc.id IS NOT NULL THEN gc.id ELSE -1 END AS growth_curve_component_id FROM growth_curve_component gc INNER JOIN species s ON s.id = gc.species_id INNER JOIN forest_type ft ON ft.id = s.forest_type_id WHERE gc.growth_curve_id = {var:growth_curve_id} AND LOWER(ft.name) LIKE LOWER('Softwood')) AS gc INNER JOIN growth_curve_component_value gcv ON gc.growth_curve_component_id = gcv.growth_curve_component_id GROUP BY gcv.age", + "type": "SQLQueryTransform", + "library": "internal.flint", + "provider": "SQLite" + } + }, + "reporting_classifiers": { + "transform": { + "allow_nulls": true, + "type": "CompositeTransform", + "library": "internal.flint", + "vars": [ + "classifier_set" + ] + } + }, + "land_class_transitions": { + "transform": { + "queryString": "SELECT dt.name AS disturbance_type, lc.code AS land_class_transition, lc.is_forest, lc.years_to_permanent FROM disturbance_type dt INNER JOIN land_class lc ON dt.transition_land_class_id = lc.id", + "type": "SQLQueryTransform", + "library": "internal.flint", + "provider": "SQLite" + } + }, + "transition_rules": { + "transform": { + "queryString": "SELECT t.id AS id, age, regen_delay, description, tt.name AS reset_type FROM transition t INNER JOIN transition_type tt ON t.transition_type_id = tt.id", + "type": "SQLQueryTransform", + "library": "internal.flint", + "provider": "SQLite" + } + }, + "transition_rule_matches": { + "transform": { + "classifier_set_var": "classifier_set", + "type": "TransitionRuleTransform", + "library": "moja.modules.cbm", + "provider": "SQLite" + } + }, + "spatial_unit_id": { + "transform": { + "queryString": "SELECT spu.id FROM spatial_unit spu INNER JOIN admin_boundary a ON spu.admin_boundary_id = a.id INNER JOIN eco_boundary e ON spu.eco_boundary_id = e.id WHERE a.name = {var:admin_boundary} AND e.name = {var:eco_boundary}", + "type": "SQLQueryTransform", + "library": "internal.flint", + "provider": "SQLite" + } + }, + "hardwood_yield_table": { + "transform": { + "queryString": "SELECT gcv.age AS age, SUM(gcv.merchantable_volume) AS merchantable_volume FROM (SELECT CASE WHEN gc.id IS NOT NULL THEN gc.id ELSE -1 END AS growth_curve_component_id FROM growth_curve_component gc INNER JOIN species s ON s.id = gc.species_id INNER JOIN forest_type ft ON ft.id = s.forest_type_id WHERE gc.growth_curve_id = {var:growth_curve_id} AND LOWER(ft.name) LIKE LOWER('Hardwood')) AS gc INNER JOIN growth_curve_component_value gcv ON gc.growth_curve_component_id = gcv.growth_curve_component_id GROUP BY gcv.age", + "type": "SQLQueryTransform", + "library": "internal.flint", + "provider": "SQLite" + } + }, + "turnover_rates": { + "transform": { + "queryString": "SELECT COALESCE(sw_turnover.foliage, 0) AS sw_foliage_turnover, COALESCE(hw_turnover.foliage, 0) AS hw_foliage_turnover, COALESCE(sw_turnover.stem, 0) AS sw_stem_turnover, COALESCE(hw_turnover.stem, 0) AS hw_stem_turnover, COALESCE(sw_turnover.branch, 0) AS sw_branch_turnover, COALESCE(hw_turnover.branch, 0) AS hw_branch_turnover, COALESCE(sw_turnover.branch_snag_split, 0) AS sw_other_to_branch_snag_split, COALESCE(hw_turnover.branch_snag_split, 0) AS hw_other_to_branch_snag_split, COALESCE(sw_turnover.stem_snag, 0) AS sw_stem_snag_turnover, COALESCE(hw_turnover.stem_snag, 0) AS hw_stem_snag_turnover, COALESCE(sw_turnover.branch_snag, 0) AS sw_branch_snag_turnover, COALESCE(hw_turnover.branch_snag, 0) AS hw_branch_snag_turnover, COALESCE(sw_turnover.coarse_ag_split, 0) AS sw_coarse_root_split, COALESCE(hw_turnover.coarse_ag_split, 0) AS hw_coarse_root_split, COALESCE(sw_turnover.coarse_root, 0) AS sw_coarse_root_turnover, COALESCE(hw_turnover.coarse_root, 0) AS hw_coarse_root_turnover, COALESCE(sw_turnover.fine_ag_split, 0) AS sw_fine_root_ag_split, COALESCE(hw_turnover.fine_ag_split, 0) AS hw_fine_root_ag_split, COALESCE(sw_turnover.fine_root, 0) AS sw_fine_root_turnover, COALESCE(hw_turnover.fine_root, 0) AS hw_fine_root_turnover FROM growth_curve gc LEFT JOIN ( SELECT growth_curve_id, foliage, stem, branch, branch_snag_split, stem_snag, branch_snag, coarse_ag_split, coarse_root, fine_ag_split, fine_root FROM turnover_parameter_association tpa INNER JOIN eco_boundary e ON tpa.eco_boundary_id = e.id INNER JOIN genus g ON tpa.genus_id = g.id INNER JOIN species s ON s.genus_id = g.id INNER JOIN forest_type f ON s.forest_type_id = f.id INNER JOIN growth_curve_component gcc ON gcc.species_id = s.id INNER JOIN turnover_parameter t ON tpa.turnover_parameter_id = t.id WHERE gcc.growth_curve_id = {var:growth_curve_id} AND e.name = {var:eco_boundary} AND f.name = 'Softwood' ORDER BY gcc.id LIMIT 1 ) AS sw_turnover ON gc.id = sw_turnover.growth_curve_id LEFT JOIN ( SELECT growth_curve_id, foliage, stem, branch, branch_snag_split, stem_snag, branch_snag, coarse_ag_split, coarse_root, fine_ag_split, fine_root FROM turnover_parameter_association tpa INNER JOIN eco_boundary e ON tpa.eco_boundary_id = e.id INNER JOIN genus g ON tpa.genus_id = g.id INNER JOIN species s ON s.genus_id = g.id INNER JOIN forest_type f ON s.forest_type_id = f.id INNER JOIN growth_curve_component gcc ON gcc.species_id = s.id INNER JOIN turnover_parameter t ON tpa.turnover_parameter_id = t.id WHERE gcc.growth_curve_id = {var:growth_curve_id} AND e.name = {var:eco_boundary} AND f.name = 'Hardwood' ORDER BY gcc.id LIMIT 1 ) AS hw_turnover ON gc.id = hw_turnover.growth_curve_id WHERE gc.id = {var:growth_curve_id}", + "type": "SQLQueryTransform", + "library": "internal.flint", + "provider": "SQLite" + } + }, + "disturbance_type_codes": { + "transform": { + "queryString": "SELECT dt.name AS disturbance_type, dt.code AS disturbance_type_code FROM disturbance_type dt", + "type": "SQLQueryTransform", + "library": "internal.flint", + "provider": "SQLite" + } + }, + "transition_rule_classifiers": { + "transform": { + "queryString": "SELECT t.id, c.name AS classifier_name, cv.value AS classifier_value FROM transition t INNER JOIN transition_classifier_value tcv ON t.id = tcv.transition_id INNER JOIN classifier_value cv ON tcv.classifier_value_id = cv.id INNER JOIN classifier c ON cv.classifier_id = c.id", + "type": "SQLQueryTransform", + "library": "internal.flint", + "provider": "SQLite" + } + }, + "initial_classifier_set": { + "transform": { + "type": "CompositeTransform", + "library": "internal.flint", + "vars": [ + "Classifier2", + "Classifier1" + ] + } + }, + "disturbance_matrix_associations": { + "transform": { + "queryString": "SELECT dt.name AS disturbance_type, dma.spatial_unit_id, dma.disturbance_matrix_id FROM disturbance_matrix_association dma INNER JOIN disturbance_type dt ON dma.disturbance_type_id = dt.id", + "type": "SQLQueryTransform", + "library": "internal.flint", + "provider": "SQLite" + } + }, + "other_to_branch_snag_split": { + "transform": { + "queryString": "SELECT t.branch_snag_split AS slow_mixing_rate FROM eco_boundary e INNER JOIN turnover_parameter t ON e.turnover_parameter_id = t.id WHERE e.name LIKE {var:eco_boundary}", + "type": "SQLQueryTransform", + "library": "internal.flint", + "provider": "SQLite" + } + }, + "growth_curve_id": { + "transform": { + "classifier_set_var": "classifier_set", + "type": "GrowthCurveTransform", + "library": "moja.modules.cbm", + "provider": "SQLite" + } + }, + "volume_to_biomass_parameters": { + "transform": { + "queryString": "SELECT ft.name AS forest_type, f.a as a, f.b as b, f.a_nonmerch as a_non_merch, f.b_nonmerch as b_non_merch, f.k_nonmerch as k_non_merch, f.cap_nonmerch as cap_non_merch, f.a_sap as a_sap, f.b_sap as b_sap, f.k_sap as k_sap, f.cap_sap as cap_sap, f.a1 as a1, f.a2 as a2, f.a3 as a3, f.b1 as b1, f.b2 as b2, f.b3 as b3, f.c1 as c1, f.c2 as c2, f.c3 as c3, f.min_volume as min_volume, f.max_volume as max_volume, f.low_stemwood_prop as low_stemwood_prop, f.high_stemwood_prop as high_stemwood_prop, f.low_stembark_prop as low_stembark_prop, f.high_stembark_prop as high_stembark_prop, f.low_branches_prop AS low_branches_prop, f.high_branches_prop as high_branches_prop, f.low_foliage_prop AS low_foliage_prop, f.high_foliage_prop AS high_foliage_prop, sp.sw_top_proportion AS softwood_top_prop, sp.sw_stump_proportion AS softwood_stump_prop, sp.hw_top_proportion AS hardwood_top_prop, sp.hw_stump_proportion AS hardwood_stump_prop, rp.hw_a AS hw_a, rp.hw_b AS hw_b, rp.sw_a AS sw_a, rp.frp_a AS frp_a, rp.frp_b AS frp_b, rp.frp_c AS frp_c FROM vol_to_bio_factor_association fa INNER JOIN vol_to_bio_factor f ON f.id = fa.vol_to_bio_factor_id INNER JOIN species s ON fa.species_id = s.id INNER JOIN growth_curve_component gcc ON s.id = gcc.species_id INNER JOIN forest_type ft ON s.forest_type_id = ft.id INNER JOIN spatial_unit spu ON fa.spatial_unit_id = spu.id INNER JOIN admin_boundary a ON spu.admin_boundary_id = a.id INNER JOIN stump_parameter sp ON a.stump_parameter_id = sp.id INNER JOIN root_parameter rp ON rp.id = fa.root_parameter_id WHERE gcc.growth_curve_id = {var:growth_curve_id} AND spu.id = {var:spatial_unit_id} ORDER BY gcc.id DESC", + "type": "SQLQueryTransform", + "library": "internal.flint", + "provider": "SQLite" + } + }, + "spu": { + "transform": { + "queryString": "select s.id AS spu_id from spatial_unit s inner join admin_boundary a on s.admin_boundary_id = a.id inner join eco_boundary e on s.eco_boundary_id = e.id where a.name like {var:admin_boundary} and e.name like {var:eco_boundary}", + "type": "SQLQueryTransform", + "library": "internal.flint", + "provider": "SQLite" + } + }, + "land_class_data": { + "transform": { + "queryString": "SELECT code AS land_class, is_forest, years_to_permanent FROM land_class lc", + "type": "SQLQueryTransform", + "library": "internal.flint", + "provider": "SQLite" + } + }, + "mean_annual_temperature": { + "transform": { + "library": "internal.flint", + "type": "LocationIdxFromFlintDataTransform", + "provider": "RasterTiled", + "data_id": "mean_annual_temperature" + } + }, + "decay_parameters": { + "transform": { + "queryString": "SELECT p.name AS pool, dp.base_decay_rate AS organic_matter_decay_rate, dp.prop_to_atmosphere AS prop_to_atmosphere, dp.q10 AS q10, dp.reference_temp AS reference_temp, dp.max_rate AS max_decay_rate_soft FROM decay_parameter dp INNER JOIN dom_pool dom ON dp.dom_pool_id = dom.id INNER JOIN pool p ON p.id = dom.pool_id", + "type": "SQLQueryTransform", + "library": "internal.flint", + "provider": "SQLite" + } + }, + "disturbances_2012": { + "transform": { + "library": "internal.flint", + "type": "LocationIdxFromFlintDataTransform", + "provider": "RasterTiled", + "data_id": "disturbances_2012" + } + }, + "disturbances_2011": { + "transform": { + "library": "internal.flint", + "type": "LocationIdxFromFlintDataTransform", + "provider": "RasterTiled", + "data_id": "disturbances_2011" + } + }, + "disturbances_2015": { + "transform": { + "library": "internal.flint", + "type": "LocationIdxFromFlintDataTransform", + "provider": "RasterTiled", + "data_id": "disturbances_2015" + } + }, + "disturbances_2014": { + "transform": { + "library": "internal.flint", + "type": "LocationIdxFromFlintDataTransform", + "provider": "RasterTiled", + "data_id": "disturbances_2014" + } + }, + "disturbances_2018": { + "transform": { + "library": "internal.flint", + "type": "LocationIdxFromFlintDataTransform", + "provider": "RasterTiled", + "data_id": "disturbances_2018" + } + }, + "disturbances_2016": { + "transform": { + "library": "internal.flint", + "type": "LocationIdxFromFlintDataTransform", + "provider": "RasterTiled", + "data_id": "disturbances_2016" + } + }, + "Classifier2": { + "transform": { + "library": "internal.flint", + "type": "LocationIdxFromFlintDataTransform", + "provider": "RasterTiled", + "data_id": "Classifier2" + } + }, + "Classifier1": { + "transform": { + "library": "internal.flint", + "type": "LocationIdxFromFlintDataTransform", + "provider": "RasterTiled", + "data_id": "Classifier1" + } + }, + "disturbances_2013": { + "transform": { + "library": "internal.flint", + "type": "LocationIdxFromFlintDataTransform", + "provider": "RasterTiled", + "data_id": "disturbances_2013" + } + } + } +} \ No newline at end of file