From b2e6816e6c928e8f5454e9ac14f9d86670a34b1f Mon Sep 17 00:00:00 2001 From: 1felipecoelho Date: Fri, 15 Nov 2024 18:28:21 +0000 Subject: [PATCH] felipe-coelho-solution --- README.md | 147 ++++++------------ dockerfile | 19 +++ requirements.txt | 4 + src/api/__pycache__/main.cpython-310.pyc | Bin 0 -> 3502 bytes src/api/main.py | 93 +++++++++++ .../example_payloads}/payload1.json | 108 ++++++------- .../example_payloads}/payload2.json | 108 ++++++------- .../example_payloads}/payload3.json | 108 ++++++------- .../example_payloads}/response3.json | 0 tests/__pycache__/test.cpython-310.pyc | Bin 0 -> 2815 bytes tests/test.py | 102 ++++++++++++ 11 files changed, 428 insertions(+), 261 deletions(-) create mode 100644 dockerfile create mode 100644 requirements.txt create mode 100644 src/api/__pycache__/main.cpython-310.pyc create mode 100644 src/api/main.py rename {example_payloads => src/example_payloads}/payload1.json (94%) rename {example_payloads => src/example_payloads}/payload2.json (94%) rename {example_payloads => src/example_payloads}/payload3.json (94%) rename {example_payloads => src/example_payloads}/response3.json (100%) create mode 100644 tests/__pycache__/test.cpython-310.pyc create mode 100644 tests/test.py diff --git a/README.md b/README.md index 44c93d608..69406d981 100644 --- a/README.md +++ b/README.md @@ -1,99 +1,48 @@ -# powerplant-coding-challenge - - -## Welcome ! - -Below you can find the description of a coding challenge that we ask people to perform when applying for a job in our team. - -The goal of this coding challenge is to provide the applicant some insight into the business we're in and as such provide the applicant an indication about the challenges she/he will be confronted with. Next, during the first interview we will use the applicant's implementation as a seed to discuss all kinds of interesting software engineering topics. - -Time is scarce, we know. Therefore we ask you not to spend more than 4 hours on this challenge. We know it is not possible to deliver a finished implementation of the challenge in only four hours. Even though your submission will not be complete, it will provide us plenty of information and topics to discuss later on during the talks. - -This coding-challenge is part of a formal process and is used in collaboration with the recruiting companies we work with. Submitting a pull-request will not automatically trigger the recruitement process. -## Who are we - -We are the IS team of the 'Short-term Power as-a-Service' (a.k.a. SPaaS) team within [GEM](https://gems.engie.com/). - -[GEM](https://gems.engie.com/), which stands for 'Global Energy Management', is the energy management arm of [ENGIE](https://www.engie.com/), one of the largest global energy players, -with access to local markets all over the world. - -SPaaS is a team consisting of around 100 people with experience in energy markets, IT and modeling. In smaller teams consisting of a mix of people with different experiences, we are active on the [day-ahead](https://en.wikipedia.org/wiki/European_Power_Exchange#Day-ahead_markets) market, [intraday markets](https://en.wikipedia.org/wiki/European_Power_Exchange#Intraday_markets) and [collaborate with the TSO to balance the grid continuously](https://en.wikipedia.org/wiki/Transmission_system_operator#Electricity_market_operations). - -## The challenge - -### In short -Calculate how much power each of a multitude of different [powerplants](https://en.wikipedia.org/wiki/Power_station) need to produce (a.k.a. the production-plan) when the [load](https://en.wikipedia.org/wiki/Load_profile) is given and taking into account the cost of the underlying energy sources (gas, kerosine) and the Pmin and Pmax of each powerplant. - -### More in detail - -The load is the continuous demand of power. The total load at each moment in time is forecasted. For instance for Belgium you can see the load forecasted by the grid operator [here](https://www.elia.be/en/grid-data/load-and-load-forecasts). - -At any moment in time, all available powerplants need to generate the power to exactly match the load. The cost of generating power can be different for every powerplant and is dependent on external factors: The cost of producing power using a [turbojet](https://en.wikipedia.org/wiki/Gas_turbine#Industrial_gas_turbines_for_power_generation), that runs on kerosine, is higher compared to the cost of generating power using a gas-fired powerplant because of gas being cheaper compared to kerosine and because of the [thermal efficiency](https://en.wikipedia.org/wiki/Thermal_efficiency) of a gas-fired powerplant being around 50% (2 units of gas will generate 1 unit of electricity) while that of a turbojet is only around 30%. The cost of generating power using windmills however is zero. Thus deciding which powerplants to activate is dependent on the [merit-order](https://en.wikipedia.org/wiki/Merit_order). - -When deciding which powerplants in the merit-order to activate (a.k.a. [unit-commitment problem](https://en.wikipedia.org/wiki/Unit_commitment_problem_in_electrical_power_production)) the maximum amount of power each powerplant can produce (Pmax) obviously needs to be taken into account. Additionally gas-fired powerplants generate a certain minimum amount of power when switched on, called the Pmin. - - -### Performing the challenge - -Build a REST API exposing an endpoint `/productionplan` that accepts a POST of which the body contains a payload as you can find in the `example_payloads` directory and that returns a json with the same structure as in `example_response.json` and that manages and logs run-time errors. - -For calculating the unit-commitment, we prefer you not to rely on an existing (linear-programming) solver but instead write an algorithm yourself. - -Implementations can be submitted in either C# (on .Net 5 or higher) or Python (3.8 or higher) as these are (currently) the main languages we use in SPaaS. Along with the implementation should be a README that describes how to compile (if applicable) and launch the application. - -- C# implementations should contain a project file to compile the application. -- Python implementations should contain a `requirements.txt` or a `pyproject.toml` (for use with poetry) to install all needed dependencies. - -#### Payload - -The payload contains 3 types of data: - - load: The load is the amount of energy (MWh) that need to be generated during one hour. - - fuels: based on the cost of the fuels of each powerplant, the merit-order can be determined which is the starting point for deciding which powerplants should be switched on and how much power they will deliver. Wind-turbine are either switched-on, and in that case generate a certain amount of energy depending on the % of wind, or can be switched off. - - gas(euro/MWh): the price of gas per MWh. Thus if gas is at 6 euro/MWh and if the efficiency of the powerplant is 50% (i.e. 2 units of gas will generate one unit of electricity), the cost of generating 1 MWh is 12 euro. - - kerosine(euro/Mwh): the price of kerosine per MWh. - - co2(euro/ton): the price of emission allowances (optionally to be taken into account). - - wind(%): percentage of wind. Example: if there is on average 25% wind during an hour, a wind-turbine with a Pmax of 4 MW will generate 1MWh of energy. - - powerplants: describes the powerplants at disposal to generate the demanded load. For each powerplant is specified: - - name: - - type: gasfired, turbojet or windturbine. - - efficiency: the efficiency at which they convert a MWh of fuel into a MWh of electrical energy. Wind-turbines do not consume 'fuel' and thus are considered to generate power at zero price. - - pmax: the maximum amount of power the powerplant can generate. - - pmin: the minimum amount of power the powerplant generates when switched on. - -#### response - -The response should be a json as in `example_payloads/response3.json`, which is the expected answer for `example_payloads/payload3.json`, specifying for each powerplant how much power each powerplant should deliver. The power produced by each powerplant has to be a multiple of 0.1 Mw and the sum of the power produced by all the powerplants together should equal the load. - -### Want more challenge? - -Having fun with this challenge and want to make it more realistic. Optionally, do one of the extra's below: - -#### Docker - -Provide a Dockerfile along with the implementation to allow deploying your solution quickly. - -#### CO2 - -Taken into account that a gas-fired powerplant also emits CO2, the cost of running the powerplant should also take into account the cost of the [emission allowances](https://en.wikipedia.org/wiki/Carbon_emission_trading). For this challenge, you may take into account that each MWh generated creates 0.3 ton of CO2. - -## Acceptance criteria - -For a submission to be reviewed as part of an application for a position in the team, the project needs to: - - contain a README.md explaining how to build and launch the API - - expose the API on port `8888` - -Failing to comply with any of these criteria will automatically disqualify the submission. - -## More info - -For more info on energy management, check out: - - - [Global Energy Management Solutions](https://www.youtube.com/watch?v=SAop0RSGdHM) - - [COO hydroelectric power station](https://www.youtube.com/watch?v=edamsBppnlg) - - [Management of supply](https://www.youtube.com/watch?v=eh6IIQeeX3c) - video made during winter 2018-2019 - -## FAQ - -##### Can an existing solver be used to calculate the unit-commitment -Implementations should not rely on an external solver and thus contain an algorithm written from scratch (clarified in the text as of version v1.1.0) - +# Power Plant Coding Challenge + +This project is based on a Fast API python code that calculates the optimal production plan for a set of Power Plants based on Load provisions and Energy type prices. + +## Project Structure +powerplant-coding-challenge/ +├── src/ +│ ├── api/ +│ │ └── main.py +│ └── example_payloads/ +├── tests/ +│ └── test.py +├── Dockerfile +├── README.md +└── requirements.txt + + +### Prerequisites + +- Docker +- Python 3.8+ +- Postman (for testing) + +### Installation +## Build the Docker image: + +$docker build -t powerplant-api . + +## Run the Docker container exposing the referenced API Port 8888: + +$docker run -p 8888:8888 powerplant-api + + +The API should be available at `http://localhost:8888`. + +### Usage + +Use Postman to send a POST request to `http://localhost:8888/productionplan` using the example payloads that can be found in the `src/example_payloads/` directory. + +Or optionally use Curl to send the Post Request (on the example below there is payload1 but there is also payload2 and payload3 to be used): + +$curl -X POST -H "Content-Type: application/json" -d @src/example_payloads/payload1.json http://localhost:8888/productionplan + +### Running Tests +To run the unit tests execute the following command on root directory: + +$python3 -m unittest discover tests + diff --git a/dockerfile b/dockerfile new file mode 100644 index 000000000..d9eeb0d17 --- /dev/null +++ b/dockerfile @@ -0,0 +1,19 @@ +FROM python:3.9-slim + +WORKDIR /app + +# Install build dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +ENV PYTHONPATH=/app/src + +EXPOSE 8888 + +CMD ["python", "src/api/main.py", "--host", "0.0.0.0", "--port", "8888"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..4d1d7b456 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +flask +dataclasses +typing +uvicorn \ No newline at end of file diff --git a/src/api/__pycache__/main.cpython-310.pyc b/src/api/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37182f7aca51dc369c16b2ddd1aa6552f3d623f5 GIT binary patch literal 3502 zcmaJ@OOG5$5zfqdboFC;-s5?Ay^RgXz1TBmS7HT>1`B(4W!Rmu7%zqvR;k>X-L7$U zRXeNN_O!au!i==`!3f0BtYjmEgoMNu@iREqfdicO4i^p!Uu4b0W7*rSd_`tOMtl(& z5y`|vh2b}N^ltZW6O8?f7BBuhEH0y^|3D|1R%f@I$PS0(* zy;7@W=yv4w%B?bI%HFA0 zCYSFD-NXJVxgt-?H{_Woq=DErt@-BX(2+H*+<89=(z|&{t9t{LW_f8RP2#Y#yV(@E z{b2}-eKl-nO`ex!kOgg=PO)$zbzenoV)Ek8!s0So`Ym*dwYX$0@i9}DvL#<<%E2fw z(%L#$+Xthw4v4l{vy?m6qaen`*=}Fu71im4?NG(--Q13Yp33chFN~=V9&R?>-1B`R z`+i>W{azvm5zTA9e{T>(2Rl}p>3q`ngE&sIAPbW?#Tlm)B|-KlrspB8>Hd3rtrur& z?X{0nrPH-eJ5kZvRh8b&lKxshd7yNkM7!8dWEgiZw(kT{q~fmn(!Er-*Mfey)(gUT zwZE&&xUEVc)zDR#XL+3dHEwoZkLlYu{z^<;Wp%_7T+d)zQjVDeUugqRC!fEr0ZW_I zCS-qfU}_uMOMoy?QJPnewM#dfo}R>IdWyPf>gK4MrOu^phB}A3Bj}3R0?|&PqYqMg zQKaA$pW=p^R}6kWSymGUd6lmB`#Nl^GVq(rV0$}^mF8fOd)*-Igj&hr1ZL;eKqqS- z-MrHr&fHZxNpbF&24AKHi8>Pjxiwh^U3}Z+lgRYM zyCRWEI&!r1k8q0NH& z*|ej^Kvm;7?t<A1o_@Nz9wi{98<-rPZ(8P=nTMpmj_*#$OfE_B&d9uo3Ax=Z?`L8R zM;nP9Nc~$Q@u9qwMZj_uL(aqEwY?OPD(ezC+z9(U95cc$eyOlW=TFE+N|Jo<5EBX& zzaZHvV-7cEj~F5`;_iAx1QSS&gyy}M*SjiK5BqxK`^1)X z7oVJF!?Ul4^6-NqK#|390pXxYvSEp`T=t#D(7VvMflPy0#cWP~PGE{Ixh!Iu4=+N}T&jH_Uqq{|zk*k6P5EERv+kd@2StS;S;-O;1RZ zPv&J64|+-?NSkC<`e4#%C6Dnq@8u0yL>C&qC zhkrfG*bP+b_8pSX?h{y!RJRA+<|37_DBm^tk}+FhtPNu2LPLODlxY+N^XgFruToty zl-ofXwyz{{CxkVwZN9%{3cuWf)l48P>~6|Es_1f?0z+YPO1s9Q@Bt&cr3di~Ve&=4 oFUs-_dPynNL3QXMhu3)B@`M0j!kgmD?sBPKuW;`h$m;+5KR) float: + #Calculate the production cost for a given PowerPlant type. + if plant_type == "windturbine": + return 0 + fuel_type = "gas(euro/MWh)" if plant_type == "gasfired" else "kerosine(euro/MWh)" + return fuel_prices.get(fuel_type, 0) / efficiency + +def calculate_power_output(plant: Plant, remaining_load: float, fuels: dict) -> float: + #Calculate the PowerPlant capacity based on remaining load and fuel data. + if plant.type == "windturbine": + wind_percentage = fuels.get("wind(%)", 0) / 100 + return min(plant.pmax * wind_percentage, remaining_load) + + if remaining_load <= 0: + return 0.0 + + if plant.type in ["gasfired", "turbojet"]: + return min(max(plant.pmin, remaining_load), plant.pmax) + + return 0.0 + +@app.route('/productionplan', methods=['POST']) +def calculate_costs(): + #API endpoint to handle POSTs requests and calculate the production plan. + try: + request_data = request.get_json() + if not request_data: + return jsonify({"error": "Invalid JSON input"}), 400 + + production_plan_request = ProductionPlanRequest(**request_data) + plants = [Plant(**plant_data) for plant_data in production_plan_request.powerplants] + results = [] + remaining_load = production_plan_request.load + + # Process plants in merit-order: wind, gas, turbojet + plant_types_order = ["windturbine", "gasfired", "turbojet"] + plant_order = {plant.name: i for i, plant in enumerate(plants)} + + for plant_type in plant_types_order: + for plant in (p for p in plants if p.type == plant_type): + cost = production_cost(plant.type, plant.efficiency, production_plan_request.fuels) + power = calculate_power_output(plant, remaining_load, production_plan_request.fuels) + + remaining_load = max(0, remaining_load - power) + + results.append({ + "name": plant.name, + "p": round(power, 1) + }) + + logging.info(f"Plant: {plant.name}, Type: {plant.type}, Output: {power:.1f} MW, Cost: {cost:.2f} €/MWh") + + results.sort(key=lambda x: plant_order[x["name"]]) + + return jsonify(results) + + #Error handling and Loggings. + except (TypeError, KeyError) as e: + logging.error(f"Invalid input data: {str(e)}") + return jsonify({"error": "Invalid input data"}), 400 + except Exception as e: + logging.error(f"Error processing request: {str(e)}") + return jsonify({"error": "Internal server error"}), 500 + +#App run if payload ok +if __name__ == '__main__': + app.run(host='0.0.0.0', port=8888, debug=True) \ No newline at end of file diff --git a/example_payloads/payload1.json b/src/example_payloads/payload1.json similarity index 94% rename from example_payloads/payload1.json rename to src/example_payloads/payload1.json index b377475fb..28b2c32c5 100644 --- a/example_payloads/payload1.json +++ b/src/example_payloads/payload1.json @@ -1,54 +1,54 @@ -{ - "load": 480, - "fuels": - { - "gas(euro/MWh)": 13.4, - "kerosine(euro/MWh)": 50.8, - "co2(euro/ton)": 20, - "wind(%)": 60 - }, - "powerplants": [ - { - "name": "gasfiredbig1", - "type": "gasfired", - "efficiency": 0.53, - "pmin": 100, - "pmax": 460 - }, - { - "name": "gasfiredbig2", - "type": "gasfired", - "efficiency": 0.53, - "pmin": 100, - "pmax": 460 - }, - { - "name": "gasfiredsomewhatsmaller", - "type": "gasfired", - "efficiency": 0.37, - "pmin": 40, - "pmax": 210 - }, - { - "name": "tj1", - "type": "turbojet", - "efficiency": 0.3, - "pmin": 0, - "pmax": 16 - }, - { - "name": "windpark1", - "type": "windturbine", - "efficiency": 1, - "pmin": 0, - "pmax": 150 - }, - { - "name": "windpark2", - "type": "windturbine", - "efficiency": 1, - "pmin": 0, - "pmax": 36 - } - ] -} +{ + "load": 480, + "fuels": + { + "gas(euro/MWh)": 13.4, + "kerosine(euro/MWh)": 50.8, + "co2(euro/ton)": 20, + "wind(%)": 60 + }, + "powerplants": [ + { + "name": "gasfiredbig1", + "type": "gasfired", + "efficiency": 0.53, + "pmin": 100, + "pmax": 460 + }, + { + "name": "gasfiredbig2", + "type": "gasfired", + "efficiency": 0.53, + "pmin": 100, + "pmax": 460 + }, + { + "name": "gasfiredsomewhatsmaller", + "type": "gasfired", + "efficiency": 0.37, + "pmin": 40, + "pmax": 210 + }, + { + "name": "tj1", + "type": "turbojet", + "efficiency": 0.3, + "pmin": 0, + "pmax": 16 + }, + { + "name": "windpark1", + "type": "windturbine", + "efficiency": 1, + "pmin": 0, + "pmax": 150 + }, + { + "name": "windpark2", + "type": "windturbine", + "efficiency": 1, + "pmin": 0, + "pmax": 36 + } + ] +} diff --git a/example_payloads/payload2.json b/src/example_payloads/payload2.json similarity index 94% rename from example_payloads/payload2.json rename to src/example_payloads/payload2.json index f3c7525db..cadf659c2 100644 --- a/example_payloads/payload2.json +++ b/src/example_payloads/payload2.json @@ -1,54 +1,54 @@ -{ - "load": 480, - "fuels": - { - "gas(euro/MWh)": 13.4, - "kerosine(euro/MWh)": 50.8, - "co2(euro/ton)": 20, - "wind(%)": 0 - }, - "powerplants": [ - { - "name": "gasfiredbig1", - "type": "gasfired", - "efficiency": 0.53, - "pmin": 100, - "pmax": 460 - }, - { - "name": "gasfiredbig2", - "type": "gasfired", - "efficiency": 0.53, - "pmin": 100, - "pmax": 460 - }, - { - "name": "gasfiredsomewhatsmaller", - "type": "gasfired", - "efficiency": 0.37, - "pmin": 40, - "pmax": 210 - }, - { - "name": "tj1", - "type": "turbojet", - "efficiency": 0.3, - "pmin": 0, - "pmax": 16 - }, - { - "name": "windpark1", - "type": "windturbine", - "efficiency": 1, - "pmin": 0, - "pmax": 150 - }, - { - "name": "windpark2", - "type": "windturbine", - "efficiency": 1, - "pmin": 0, - "pmax": 36 - } - ] -} +{ + "load": 480, + "fuels": + { + "gas(euro/MWh)": 13.4, + "kerosine(euro/MWh)": 50.8, + "co2(euro/ton)": 20, + "wind(%)": 0 + }, + "powerplants": [ + { + "name": "gasfiredbig1", + "type": "gasfired", + "efficiency": 0.53, + "pmin": 100, + "pmax": 460 + }, + { + "name": "gasfiredbig2", + "type": "gasfired", + "efficiency": 0.53, + "pmin": 100, + "pmax": 460 + }, + { + "name": "gasfiredsomewhatsmaller", + "type": "gasfired", + "efficiency": 0.37, + "pmin": 40, + "pmax": 210 + }, + { + "name": "tj1", + "type": "turbojet", + "efficiency": 0.3, + "pmin": 0, + "pmax": 16 + }, + { + "name": "windpark1", + "type": "windturbine", + "efficiency": 1, + "pmin": 0, + "pmax": 150 + }, + { + "name": "windpark2", + "type": "windturbine", + "efficiency": 1, + "pmin": 0, + "pmax": 36 + } + ] +} diff --git a/example_payloads/payload3.json b/src/example_payloads/payload3.json similarity index 94% rename from example_payloads/payload3.json rename to src/example_payloads/payload3.json index bd28884ce..9d7416928 100644 --- a/example_payloads/payload3.json +++ b/src/example_payloads/payload3.json @@ -1,54 +1,54 @@ -{ - "load": 910, - "fuels": - { - "gas(euro/MWh)": 13.4, - "kerosine(euro/MWh)": 50.8, - "co2(euro/ton)": 20, - "wind(%)": 60 - }, - "powerplants": [ - { - "name": "gasfiredbig1", - "type": "gasfired", - "efficiency": 0.53, - "pmin": 100, - "pmax": 460 - }, - { - "name": "gasfiredbig2", - "type": "gasfired", - "efficiency": 0.53, - "pmin": 100, - "pmax": 460 - }, - { - "name": "gasfiredsomewhatsmaller", - "type": "gasfired", - "efficiency": 0.37, - "pmin": 40, - "pmax": 210 - }, - { - "name": "tj1", - "type": "turbojet", - "efficiency": 0.3, - "pmin": 0, - "pmax": 16 - }, - { - "name": "windpark1", - "type": "windturbine", - "efficiency": 1, - "pmin": 0, - "pmax": 150 - }, - { - "name": "windpark2", - "type": "windturbine", - "efficiency": 1, - "pmin": 0, - "pmax": 36 - } - ] -} +{ + "load": 910, + "fuels": + { + "gas(euro/MWh)": 13.4, + "kerosine(euro/MWh)": 50.8, + "co2(euro/ton)": 20, + "wind(%)": 60 + }, + "powerplants": [ + { + "name": "gasfiredbig1", + "type": "gasfired", + "efficiency": 0.53, + "pmin": 100, + "pmax": 460 + }, + { + "name": "gasfiredbig2", + "type": "gasfired", + "efficiency": 0.53, + "pmin": 100, + "pmax": 460 + }, + { + "name": "gasfiredsomewhatsmaller", + "type": "gasfired", + "efficiency": 0.37, + "pmin": 40, + "pmax": 210 + }, + { + "name": "tj1", + "type": "turbojet", + "efficiency": 0.3, + "pmin": 0, + "pmax": 16 + }, + { + "name": "windpark1", + "type": "windturbine", + "efficiency": 1, + "pmin": 0, + "pmax": 150 + }, + { + "name": "windpark2", + "type": "windturbine", + "efficiency": 1, + "pmin": 0, + "pmax": 36 + } + ] +} diff --git a/example_payloads/response3.json b/src/example_payloads/response3.json similarity index 100% rename from example_payloads/response3.json rename to src/example_payloads/response3.json diff --git a/tests/__pycache__/test.cpython-310.pyc b/tests/__pycache__/test.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36915d1d42c717435d91abb006dd911dbe36df46 GIT binary patch literal 2815 zcmbtWOK%)S5bmDG?(D;k<2Vl|5FAKiZ$ftSkRUi#5|l(yBw|H*ffm9rnI3P(o}C$Y z_r}R;EkKS0IfTcB3qi7vzT`)6;t(!yKx%OSaoQgMNCpL}dcBU91O%d2Q`6O5T~pQd zRn;baeR%>Sy!&$P-3^5Ngv|P30do=_aS;e3j7Fr2ZzH00qiRr`n~_n+Y;#aPSfQ!nMp9E3mbRcGAl2+1WF5oG8e!x!{ z96sRR@S-S)Yr#M6;ip6eX*s>EY!S#Sse^W7^zyVwo7*nKX0NwpE!N|67R3ADL2txS zxB}G(=F41`tcAS%Q26cQ$*#2SC%D~Qxpy|Lc86qsdV@>DmEscuuWcU%> zV(sgPIQE^aP3dv&WP1qiwmku)WHtM0o)`-nJgJ<2{YI%dI1}(hgmJKLtY&@q>6BKD&zsX9@Y*ir_0rT8GGyFFMs~wM4Q1U!iNyUlC7-Rs|U)Gx6?r7 zgQ=;|2Z;K&m6g`R7^!!wN8w?$wX0Ah^ybFz#vFz+U`2_?ls(l5 zBB2V8GefLwsHAIE0h48A04hcl`W}{v3RaP2Ls`s|o+|oDEFl|RbiMr*8k7Oj1A0ty z4S0BqZ9v`!I9MVw-Q9h__ovV7KLke(QKY>IpPechUiR0xpMSKrvf=ZHs5Bh<4!hRdJ zFX6{_0;!?>g|7XJki<)r8!h^&F>9a~d=m&MryCPcn-|jKQp|<&#G7+uBk<;SlIF4h?$?^0Cp^K5bp(|ozH}rubUlI8gLps}0bdwhiwyc? zYM=O4IMWFbe+j0`lpliEV-NUYVBdgeH7I)tA4LJ}>|UHf3nZofPC2(yhz3U|mCYLU z6kzBHsM_+(Y{QFG0WM881jLL5%0{#bz8{4LI#TDvxfn!ZA2JX9RVEa2Wr{{!ZR|+A z8r9(o9c>mMMwYTXhZ=Gk78=104@8>8@L8?9AQS0Du1<$eF1Z?mLggWe-J~JYhU_?e z6f|)S$f2j94C}nO3B)krNAYLNe@?zI7AQ0jXkx%; zx;!)vz5ll6^p$8!xVgWu$|G@sZ}>+R|## z86OEh!TRfHIPq8TFrtZ%1A*GX!&q|c1Uj_*7|a038UV#GzUf6Fa}fjl49ff^7YB7r zyJBGsVSf|E1lC)J;t%hBz=V6J%{_m2*FEPaOBv<3F7^u7Re9H~!+);`=S9~=$ai*l z5zRS=`=}`$A{zCX+%E|hZN}$V9|NINUcBq E1m>yP4FCWD literal 0 HcmV?d00001 diff --git a/tests/test.py b/tests/test.py new file mode 100644 index 000000000..6607ebc1d --- /dev/null +++ b/tests/test.py @@ -0,0 +1,102 @@ +import unittest +from flask import json +from src.api.main import app, Plant, ProductionPlanRequest + +#test running capabilities +class TestProductionPlan(unittest.TestCase): + #Setup for testing mode on flask app + def setUp(self): + self.app = app.test_client() + self.app.testing = True + + #main test method tp check the functionality of the /productionplan endpoint. + def test_calculate_costs(self): + #payload3 + test_data = { + "load": 910, + "fuels": + { + "gas(euro/MWh)": 13.4, + "kerosine(euro/MWh)": 50.8, + "co2(euro/ton)": 20, + "wind(%)": 60 + }, + "powerplants": [ + { + "name": "gasfiredbig1", + "type": "gasfired", + "efficiency": 0.53, + "pmin": 100, + "pmax": 460 + }, + { + "name": "gasfiredbig2", + "type": "gasfired", + "efficiency": 0.53, + "pmin": 100, + "pmax": 460 + }, + { + "name": "gasfiredsomewhatsmaller", + "type": "gasfired", + "efficiency": 0.37, + "pmin": 40, + "pmax": 210 + }, + { + "name": "tj1", + "type": "turbojet", + "efficiency": 0.3, + "pmin": 0, + "pmax": 16 + }, + { + "name": "windpark1", + "type": "windturbine", + "efficiency": 1, + "pmin": 0, + "pmax": 150 + }, + { + "name": "windpark2", + "type": "windturbine", + "efficiency": 1, + "pmin": 0, + "pmax": 36 + } + ] + } + + #POST request simulation to the /productionplan endpoint with the test data. + response = self.app.post('/productionplan', + data=json.dumps(test_data), + content_type='application/json') + + self.assertEqual(response.status_code, 200) + + data = json.loads(response.data) + self.assertIsInstance(data, list) + self.assertEqual(len(data), 6) # Ensure all plants are in the response + + # Check if the sum of power outputs equals the load + total_power = sum(plant['p'] for plant in data) + self.assertAlmostEqual(total_power, test_data['load'], places=1) + + # Check if wind turbines are at their maximum possible output + wind_plants = [p for p in data if p['name'].startswith('windpark')] + for plant in wind_plants: + max_output = next(p['pmax'] for p in test_data['powerplants'] if p['name'] == plant['name']) + self.assertAlmostEqual(plant['p'], max_output * test_data['fuels']['wind(%)'] / 100, places=1) + + #Test to check how API handles invalid input + def test_invalid_input(self): + invalid_data = {"load": "not a number"} + response = self.app.post('/productionplan', + data=json.dumps(invalid_data), + content_type='application/json') + print(f"Response status: {response.status_code}") + print(f"Response content: {response.data}") + self.assertEqual(response.status_code, 400) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file