From 67f7a522490086d619976bc84b1d1f6ed4e43022 Mon Sep 17 00:00:00 2001 From: Weber Date: Sun, 15 Sep 2024 17:47:44 +0200 Subject: [PATCH] Powerplants production dispatch API initiation --- .gitignore | 2 + USING_APP_README.md | 33 +++++++++++ __init__.py | 4 ++ app.py | 9 +++ blue_print.py | 7 +++ controllers/__init__.py | 1 + controllers/production_plan_controller.py | 32 +++++++++++ launch_dummy_payload.py | 23 ++++++++ models/__init__.py | 1 + models/powerplants.py | 68 +++++++++++++++++++++++ requirements.txt | 17 ++++++ 11 files changed, 197 insertions(+) create mode 100644 .gitignore create mode 100644 USING_APP_README.md create mode 100644 __init__.py create mode 100644 app.py create mode 100644 blue_print.py create mode 100644 controllers/__init__.py create mode 100644 controllers/production_plan_controller.py create mode 100644 launch_dummy_payload.py create mode 100644 models/__init__.py create mode 100644 models/powerplants.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..4dab2a5bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*env/ +*__pycache__/ diff --git a/USING_APP_README.md b/USING_APP_README.md new file mode 100644 index 000000000..d43e411b1 --- /dev/null +++ b/USING_APP_README.md @@ -0,0 +1,33 @@ + +# About the API + +## Prerequisite + +Current API was built throught Flask framework. In order to launch Flask server, some dependencies +are needed, and should be installed (see requirements.txt) + +## API using + +Of course, you need to launch the flask server. On root folder, launch app.py. + +### First way : throught script + +Always on the root, a script called *launch_dummy_payload.py* init a request attempt to the server. To use it, +just set the path to your file on the variable file_path, then launch it on CLI. + +### Second way : curl command + +Less convenient, curl command could be use, like this : *curl -X POST http://127.0.0.0:8888/production_plan -H "Content-Type: application/json" -d your_json_values*. Keep in mind that is not the most convenient way for large set of data. In this case, use the first option. + +## Interesting upgrade + +### CO2 cost inclusion + +Could be interesting to add in merit order this cost + +### Production quantity order + +Include production qty in using order, after cost; +By instance, producing load of 400 with two powerplants (efficienty and cost equals) pmin and pmax respectively at 100/300 and 200/450. +It makes sens to use second before, to avoid two powerplants activation (first cannot take the load completely, second is required, +which oversize the asked load: 300 + 200 > 400). diff --git a/__init__.py b/__init__.py new file mode 100644 index 000000000..fed80edce --- /dev/null +++ b/__init__.py @@ -0,0 +1,4 @@ +from . import controllers +from . import models +import app +import blue_print diff --git a/app.py b/app.py new file mode 100644 index 000000000..deb630ed0 --- /dev/null +++ b/app.py @@ -0,0 +1,9 @@ +from flask import Flask + +from blue_print import production_plan_blue_print + +app = Flask(__name__) +app.register_blueprint(production_plan_blue_print) + +if __name__ == "__main__": + app.run(host="127.0.0.0", port=8888) diff --git a/blue_print.py b/blue_print.py new file mode 100644 index 000000000..641c5d0e7 --- /dev/null +++ b/blue_print.py @@ -0,0 +1,7 @@ +from flask import Blueprint +from controllers.production_plan_controller import get_production_plan + +production_plan_blue_print = Blueprint("production_plan_blue_print", __name__) +production_plan_blue_print.route("/production_plan", methods=["POST"])( + get_production_plan +) diff --git a/controllers/__init__.py b/controllers/__init__.py new file mode 100644 index 000000000..10c212c51 --- /dev/null +++ b/controllers/__init__.py @@ -0,0 +1 @@ +from . import production_plan_controller diff --git a/controllers/production_plan_controller.py b/controllers/production_plan_controller.py new file mode 100644 index 000000000..a89710f3c --- /dev/null +++ b/controllers/production_plan_controller.py @@ -0,0 +1,32 @@ +from flask import request, jsonify +from models.powerplants import ( + init_all_powerplant_units, + get_merit_order_production_plan, +) + + +def get_production_plan(): + request_values = request.json + required_load = request_values.get("load", 0) + powerplant_values = request_values.get("powerplants", {}) + productivity_settings = request_values.get("fuels") + try: + assert ( + isinstance(required_load, float) or isinstance(required_load, int) + ) and required_load > 0 + powerplants = init_all_powerplant_units( + powerplant_values, productivity_settings + ) + assert len(powerplants) != 0 + load_production = get_merit_order_production_plan(required_load, powerplants) + except AssertionError: + return jsonify( + {"Error": "No valid load or powerplants missing in the given values."} + ) + except Exception as e: + return jsonify( + { + "Error": f"Some troubles append during processing. '{e}'. Please check the values" + } + ) + return jsonify(load_production) diff --git a/launch_dummy_payload.py b/launch_dummy_payload.py new file mode 100644 index 000000000..b58416456 --- /dev/null +++ b/launch_dummy_payload.py @@ -0,0 +1,23 @@ + +import requests +import json +from json import JSONDecodeError + +file_path = '' # Your filepath + +data = {} + +try: + with open(file_path, 'r') as file: + data = json.load(file) + headers = {'Content-Type': 'application/json'} + + res = requests.post( + 'http://127.0.0.0:8888/production_plan', + json.dumps(data), headers=headers) + if res.ok: + print(res.json()) + +except JSONDecodeError: + print("Given Json file is not valid.") + diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 000000000..e3a032dc5 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1 @@ +from . import powerplants diff --git a/models/powerplants.py b/models/powerplants.py new file mode 100644 index 000000000..3eaaee110 --- /dev/null +++ b/models/powerplants.py @@ -0,0 +1,68 @@ +TYPE_COST_MAPPING = {"gasfired": "gas(euro/MWh)", "turbojet": "kerosine(euro/MWh)"} + + +class PowerPlant: + + def __init__(self, values): + for key, value in values.items(): + setattr(self, key, value) + + def get_producted_load(self, required_load): + # To extend method, depending powerplant type + pass + + +class WindPowerPlant(PowerPlant): + + def __init__(self, values): + super().__init__(values) + self.wind_rate = values.get("wind") + + def get_producted_load(self, required_load): + if required_load <= 0: + return 0 + load_to_product = required_load if self.pmax > required_load else self.pmax + return round(load_to_product * self.wind_rate, 1) + + +class FossilePowerPlant(PowerPlant): + + def __init__(self, values): + super().__init__(values) + theorical_cost = values.get("cost") + self.cost = round(theorical_cost * (1 / (self.efficiency)), 1) + + def get_producted_load(self, required_load): + if required_load <= 0: + return 0 + elif self.pmin < required_load < self.pmax: + return required_load + return self.pmin if required_load < self.pmin else self.pmax + + +def init_all_powerplant_units(powerplant_values, productivity_settings): + powerplants = [] + breakpoint() + for values in powerplant_values: + type = values.get("type") + cost_type = TYPE_COST_MAPPING.get(type, "wind") + values.update( + { + "cost": productivity_settings.get(cost_type, 0.0), + "wind": productivity_settings.get("wind(%)", 0.0) / 100, + } + ) + ToInstanceClass = WindPowerPlant if type == "windturbine" else FossilePowerPlant + powerplant = ToInstanceClass(values) + powerplants.append(powerplant) + return powerplants + + +def get_merit_order_production_plan(required_load, powerplants): + production_plan = [] + ordered_powerplants = sorted(powerplants, key=lambda powerplant: powerplant.cost) + for powerplant in ordered_powerplants: + producted_load = powerplant.get_producted_load(required_load) + production_plan.append({"name": powerplant.name, "p": producted_load}) + required_load = round(required_load - producted_load, 1) + return production_plan diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..60dd5ab97 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +black==24.8.0 +blinker==1.8.2 +certifi==2024.8.30 +charset-normalizer==3.3.2 +click==8.1.7 +Flask==3.0.3 +idna==3.9 +itsdangerous==2.2.0 +Jinja2==3.1.4 +MarkupSafe==2.1.5 +mypy-extensions==1.0.0 +packaging==24.1 +pathspec==0.12.1 +platformdirs==4.3.3 +requests==2.32.3 +urllib3==2.2.3 +Werkzeug==3.0.4 \ No newline at end of file