Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Virtual environment
.venv/
env/
venv/
ENV/
env.bak/
venv.bak/

# Flask specific
instance/
.webassets-cache

# Pytest
.cache/
.pytest_cache/

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/

# Jupyter Notebook
.ipynb_checkpoints

# Environments
.env
.env.*

# IDEs
.vscode/
.idea/
*.swp
*~
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Use the official Python image from the Docker Hub
FROM python:3.9-slim

# Set the working directory in the container
WORKDIR /app

# Copy the requirements file into the container
COPY requirements.txt .

# Install the dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application code into the container
COPY . .

# Expose the port the app runs on
EXPOSE 8888

# Run the application
CMD ["python", "run.py"]
193 changes: 94 additions & 99 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,99 +1,94 @@
# 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 Production Plan API

This project is a ptyhon Flask-based API used in the code challenge intending to calculate power distribution using request parameters such as: load, fuels, and powerplants.

## Prerequisites

We assume that you need to have the following components in order to build and launch this application:

* Docker
* Docker Compose

## Project Structure
.
├── app/
│ ├── \_\_init\_\_.py # Initializes the Flask application
│ ├── routes.py # Defines the API routes
│ ├── services.py # Contains the logic for calculating power distribution
│ ├── utils.py # Defines the utils methods
├── tests/
│ ├── \_\_init\_\_.py # Initializes the unit tests module
│ ├── test_routes.py # Tests the API routes
│ ├── test_services.py # Tests the logic for calculating power distribution
├── .gitignore # Defines patterns to be ignored in the github repository
├── config.py # Defines the Flask application configuration
├── docker-compose.yml # Docker Compose file for setting up the services
├── Dockerfile # Dockerfile for building the Docker image
└── README.md # Documentation for the project
├── requirements.txt # Lists the project dependencies
├── run.py # Entry point for running the Flask application

## Setup and Installation

1. **Build the Docker image:**

```bash
docker-compose build
```

1. Run the Docker container

```bash
docker-compose build
```

1. The API will be available at http://localhost:8888.
* port 8888 is used following the requirement

## API Endpoints

```/productionplan```

* **Method**: ```POST```
* **Description**: Calculates power distribution based on load, fuels, and powerplants.
* **Request Body (schema)**:
```json
{
"load": "<< Integer containing the amount of energy (MWh) that need to be generated during one hour >>",
"fuels": {
"gas(euro/MWh)": "<< Float number containing the price of gas per MWh >>",
"kerosine(euro/MWh)": "<< Float number containing the price of kerosine per MWh >>",
"co2(euro/ton)": "<< Integer number containing the price of emission allowances >>",
"wind(%)": "<< Integer number containing the percentage of wind >>"
},
"powerplants": [
{
"name": "<< String containing the powerplant name >>",
"type": "<< Type on the powerplante (gasfired, turbojet or windturbine) >>",
"efficiency": "<< Float number containing the efficiency at which they convert a MWh of fuel into a MWh of electrical energy >>",
"pmin": "<< Number with the maximum amount of power the powerplant can generate >>",
"pmax": "<< Number with the minimum amount of power the powerplant generates when switched on >>"
}
]
}
```
* **Response (schema)**:
```json
[
{
"name": "<< string representing powerplant name >>",
"p": "<< float number representing how much power was produced >>"
}
]
```

## Running unit tests:
For unit tests executions, please run the following command:

```bash
docker-compose run test
```

## Author

* **Guilherme Cardoso Fernandes** - [GitHub Profile](https://github.com/guicfernandes)
12 changes: 12 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from flask import Flask
from config import ProductionConfig, DevelopmentConfig


def create_app(config_class=ProductionConfig):
app = Flask(__name__)
app.config.from_object(config_class)

with app.app_context():
from . import routes

return app
23 changes: 23 additions & 0 deletions app/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import logging
from flask import request, jsonify, current_app as app
from .services import calculate_power_distribution


@app.route("/productionplan", methods=["POST"])
def production_plan():
"""Calculate power distribution based on input payload."""

try:
# Parse JSON payload
payload = request.get_json()
load = payload["load"]
fuels = payload["fuels"]
powerplants = payload["powerplants"]

# Calculate power distribution
distribution = calculate_power_distribution(load, fuels, powerplants)

return jsonify(distribution), 200
except (KeyError, TypeError, ValueError) as e:
logging.error("Error processing request: %s", e)
return jsonify({"error": str(e)}), 400
61 changes: 61 additions & 0 deletions app/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from app.utils import allocate_powerplant_load


def calculate_power_distribution(load: int, fuels: dict, powerplants: list) -> list:
"""Calculate power distribution based on load, fuels and powerplants.

Args:
load (int): The amount of energy (MWh) that need to be generated during one hour.
fuels (dict): A dictionary containing the cost of different fuels.
powerplants (list): A list of powerplants with their respective properties.

Raises:
ValueError: If a property value is not accepted.

Returns:
list: A list of powerplants with their respective power output.
"""
# Parse fuels
gas_cost = fuels["gas(euro/MWh)"] / 0.5 # efficiency 50%
kerosine_cost = fuels["kerosine(euro/MWh)"] / 0.3 # efficiency 30%
wind_percentage = fuels["wind(%)"] / 100

# Calculate cost per MWh per powerplant
powerplant_costs = []
for plant in powerplants:
if plant["type"] == "gasfired":
cost = gas_cost / plant["efficiency"]
elif plant["type"] == "turbojet":
cost = kerosine_cost / plant["efficiency"]
elif plant["type"] == "windturbine":
cost = 0 # wind is free
else:
raise ValueError("Unknown powerplant type")

# Calculate effective max power for wind turbines
if plant["type"] == "windturbine":
max_power = plant["pmax"] * wind_percentage
else:
max_power = plant["pmax"]

powerplant_costs.append(
{
"name": plant["name"],
"type": plant["type"],
"cost": cost,
"pmin": plant["pmin"],
"pmax": max_power,
}
)

# Sort powerplants by cost (merit order)
sorted_plants = sorted(powerplant_costs, key=lambda x: x["cost"])

# Allocate load based on merit order
power_distribution = allocate_powerplant_load(load, sorted_plants)

# Ensure the total load matches exactly
if abs(sum(item["p"] for item in power_distribution) - load) > 0.1:
raise ValueError("Unable to satisfy load exactly with given powerplants.")

return power_distribution
Loading