diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..fe1152bdb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..c3cc4803f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.vs +/Powerplants.Api/bin +/Powerplants.Api/obj +/Powerplants.BusinessLogic/bin +/Powerplants.BusinessLogic/obj +/Powerplants.UnitTests/bin +/Powerplants.UnitTests/obj \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..fceea2040 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 8888 + + +# This stage is used to build the service project +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["./Powerplants.Api/Powerplants.Api.csproj", "Powerplants.Api/"] +COPY ["./Powerplants.BusinessLogic/Powerplants.BusinessLogic.csproj", "PowerPlant.BusinessLogic/"] +RUN dotnet restore "Powerplants.Api/Powerplants.Api.csproj" +COPY . . +WORKDIR "/src/Powerplants.Api" +RUN dotnet build "./Powerplants.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build + + +FROM build AS publish +RUN dotnet publish "Powerplants.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false + + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Powerplants.Api.dll", "--urls", "http://0.0.0.0:8888"] diff --git a/Powerplant.sln b/Powerplant.sln new file mode 100644 index 000000000..928454abf --- /dev/null +++ b/Powerplant.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35327.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Powerplants.Api", "Powerplants.Api\Powerplants.Api.csproj", "{F763667A-EB16-4025-B63F-D7BC8A2484FB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Powerplants.BusinessLogic", "Powerplants.BusinessLogic\Powerplants.BusinessLogic.csproj", "{C857BECE-7512-4BA1-9CDA-BAACCF969312}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerPlants.UnitTests", "Powerplants.UnitTests\PowerPlants.UnitTests.csproj", "{43532438-5E93-44E9-A5AB-0C66B0E6875B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F763667A-EB16-4025-B63F-D7BC8A2484FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F763667A-EB16-4025-B63F-D7BC8A2484FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F763667A-EB16-4025-B63F-D7BC8A2484FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F763667A-EB16-4025-B63F-D7BC8A2484FB}.Release|Any CPU.Build.0 = Release|Any CPU + {C857BECE-7512-4BA1-9CDA-BAACCF969312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C857BECE-7512-4BA1-9CDA-BAACCF969312}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C857BECE-7512-4BA1-9CDA-BAACCF969312}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C857BECE-7512-4BA1-9CDA-BAACCF969312}.Release|Any CPU.Build.0 = Release|Any CPU + {43532438-5E93-44E9-A5AB-0C66B0E6875B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43532438-5E93-44E9-A5AB-0C66B0E6875B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43532438-5E93-44E9-A5AB-0C66B0E6875B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43532438-5E93-44E9-A5AB-0C66B0E6875B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AE04E8DA-1227-4549-A251-F801A2A0BEF0} + EndGlobalSection +EndGlobal diff --git a/Powerplants.Api/Controllers/ProductController.cs b/Powerplants.Api/Controllers/ProductController.cs new file mode 100644 index 000000000..88501ec2f --- /dev/null +++ b/Powerplants.Api/Controllers/ProductController.cs @@ -0,0 +1,27 @@ +using BusinessLogic.Models; +using BusinessLogic.Services.Calculation; +using Microsoft.AspNetCore.Mvc; +using System.Net; + +namespace Powerplant.Controllers +{ + [ApiController] + [Route("productionplan")] + public class ProductController : ControllerBase + { + private readonly ICalculationPowerPlanService _calculationService; + + public ProductController(ICalculationPowerPlanService calculationService) + { + _calculationService = calculationService; + } + [HttpPost] + [ProducesResponseType(typeof(List), (int)HttpStatusCode.OK)] + [ProducesResponseType((int)HttpStatusCode.BadRequest)] + public IActionResult CalculatePowerPlants([FromBody] ProductPlanInput productPlanDto) + { + List result = _calculationService.CalculatePowerPlants(productPlanDto); + return Ok(result); + } + } +} diff --git a/Powerplants.Api/Powerplant.http b/Powerplants.Api/Powerplant.http new file mode 100644 index 000000000..9bbb8a7ce --- /dev/null +++ b/Powerplants.Api/Powerplant.http @@ -0,0 +1,6 @@ +@Powerplant_HostAddress = http://localhost:5041 + +GET {{Powerplant_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Powerplants.Api/Powerplants.Api.csproj b/Powerplants.Api/Powerplants.Api.csproj new file mode 100644 index 000000000..9130a0c55 --- /dev/null +++ b/Powerplants.Api/Powerplants.Api.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + 23497340-e9d2-40a4-8e7a-77c37c456ba9 + Linux + + + + + + + + + + + + diff --git a/Powerplants.Api/Powerplants.Api.csproj.user b/Powerplants.Api/Powerplants.Api.csproj.user new file mode 100644 index 000000000..c404400a7 --- /dev/null +++ b/Powerplants.Api/Powerplants.Api.csproj.user @@ -0,0 +1,9 @@ + + + + https + + + ProjectDebugger + + \ No newline at end of file diff --git a/Powerplants.Api/Program.cs b/Powerplants.Api/Program.cs new file mode 100644 index 000000000..2cf31f244 --- /dev/null +++ b/Powerplants.Api/Program.cs @@ -0,0 +1,31 @@ + +using BusinessLogic.Services.Calculation; +using System.Text.Json.Serialization; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers().AddJsonOptions(options => +{ + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); +}); +builder.Services.AddScoped(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + + +app.UseSwagger(); +app.UseSwaggerUI(); + + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Powerplants.Api/Properties/launchSettings.json b/Powerplants.Api/Properties/launchSettings.json new file mode 100644 index 000000000..899830ebe --- /dev/null +++ b/Powerplants.Api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "profiles": { + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:8888" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8888" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:56801/", + "sslPort": 8888 + } + } +} \ No newline at end of file diff --git a/Powerplants.Api/appsettings.Development.json b/Powerplants.Api/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/Powerplants.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Powerplants.Api/appsettings.json b/Powerplants.Api/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/Powerplants.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Powerplants.BusinessLogic/Enums/TypePowerEnum.cs b/Powerplants.BusinessLogic/Enums/TypePowerEnum.cs new file mode 100644 index 000000000..aa133d981 --- /dev/null +++ b/Powerplants.BusinessLogic/Enums/TypePowerEnum.cs @@ -0,0 +1,9 @@ +namespace BusinessLogic.Enums +{ + public enum TypePowerEnum + { + Gasfired, + Turbojet, + Windturbine + } +} diff --git a/Powerplants.BusinessLogic/Models/FuelInput.cs b/Powerplants.BusinessLogic/Models/FuelInput.cs new file mode 100644 index 000000000..dc5e8f0d7 --- /dev/null +++ b/Powerplants.BusinessLogic/Models/FuelInput.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace BusinessLogic.Models +{ + public class FuelInput + { + [Required] + [JsonPropertyName("gas(euro/MWh)")] + public decimal GasEuroPerMWh { get; set; } + + [Required] + [JsonPropertyName("kerosine(euro/MWh)")] + public decimal KerosineEuroPerMWh { get; set; } + + [Required] + [JsonPropertyName("co2(euro/ton)")] + public decimal Co2EuroPerTon { get; set; } + + [Required] + [JsonPropertyName("wind(%)")] + public decimal WindPercentage { get; set; } + } +} diff --git a/Powerplants.BusinessLogic/Models/PowerPlantInput.cs b/Powerplants.BusinessLogic/Models/PowerPlantInput.cs new file mode 100644 index 000000000..26360eba5 --- /dev/null +++ b/Powerplants.BusinessLogic/Models/PowerPlantInput.cs @@ -0,0 +1,23 @@ +using BusinessLogic.Enums; +using System.ComponentModel.DataAnnotations; + +namespace BusinessLogic.Models +{ + public class PowerPlantInput + { + [Required] + public string Name { get; set; } + + [Required] + public TypePowerEnum Type { get; set; } + + [Required] + public decimal Efficiency { get; set; } + + [Required] + public int Pmin { get; set; } + + [Required] + public int Pmax { get; set; } + } +} diff --git a/Powerplants.BusinessLogic/Models/ProductPlanInput.cs b/Powerplants.BusinessLogic/Models/ProductPlanInput.cs new file mode 100644 index 000000000..d49ff6e65 --- /dev/null +++ b/Powerplants.BusinessLogic/Models/ProductPlanInput.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace BusinessLogic.Models +{ + public class ProductPlanInput + { + [Required] + public int Load { get; set; } + + [Required] + public FuelInput Fuels { get; set; } + + [Required] + public List Powerplants { get; set; } + } +} diff --git a/Powerplants.BusinessLogic/Models/ProductResponse.cs b/Powerplants.BusinessLogic/Models/ProductResponse.cs new file mode 100644 index 000000000..606308dd8 --- /dev/null +++ b/Powerplants.BusinessLogic/Models/ProductResponse.cs @@ -0,0 +1,8 @@ +namespace BusinessLogic.Models +{ + public class ProductResponse + { + public string Name { get; set; } + public decimal P { get; set; } + } +} diff --git a/Powerplants.BusinessLogic/Powerplants.BusinessLogic.csproj b/Powerplants.BusinessLogic/Powerplants.BusinessLogic.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/Powerplants.BusinessLogic/Powerplants.BusinessLogic.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Powerplants.BusinessLogic/Records/PriceMwh.cs b/Powerplants.BusinessLogic/Records/PriceMwh.cs new file mode 100644 index 000000000..1870d73ec --- /dev/null +++ b/Powerplants.BusinessLogic/Records/PriceMwh.cs @@ -0,0 +1,7 @@ +namespace BusinessLogic.Records +{ + internal record PriceMwh(decimal Price, string Name) + { + + } +} diff --git a/Powerplants.BusinessLogic/Services/Calculation/CalculationPowerPlanService.cs b/Powerplants.BusinessLogic/Services/Calculation/CalculationPowerPlanService.cs new file mode 100644 index 000000000..6c6763046 --- /dev/null +++ b/Powerplants.BusinessLogic/Services/Calculation/CalculationPowerPlanService.cs @@ -0,0 +1,118 @@ +using BusinessLogic.Enums; +using BusinessLogic.Models; +using BusinessLogic.Records; + +namespace BusinessLogic.Services.Calculation +{ + public class CalculationPowerPlanService : ICalculationPowerPlanService + { + private const decimal CO2Mwh = 0.3m; + public List CalculatePowerPlants(ProductPlanInput dto) + { + List result = new List(); + if (dto.Load == 0) + { + dto.Powerplants.ForEach(powerPlant => AddProduct(powerPlant.Name, 0, result)); + return result; + } + + var groupedPowerPlants = dto.Powerplants.GroupBy(x => x.Type); + + + CalculatePowerWindTubine(groupedPowerPlants.Where(x => x.Key == TypePowerEnum.Windturbine).SelectMany(x => x), result, dto.Fuels, dto.Load); + + + var prices = CalculatePricesNotWindTubines(dto, groupedPowerPlants); + + CalculatePowerNotWindTurbines(dto, result, prices); + + return result; + } + + private List CalculatePricesNotWindTubines(ProductPlanInput dto, IEnumerable> groupedPowerPlants) + { + List prices = new List(); + foreach (var item in groupedPowerPlants.Where(x => x.Key != TypePowerEnum.Windturbine).SelectMany(x => x)) + { + if (item.Type == TypePowerEnum.Gasfired) + { + if(item.Efficiency == 0) + { + prices.Add(new PriceMwh(decimal.MaxValue, item.Name)); + continue; + } + var pricePerMhw = dto.Fuels.GasEuroPerMWh * CO2Mwh; + prices.Add(new PriceMwh(dto.Fuels.GasEuroPerMWh / item.Efficiency + pricePerMhw, item.Name)); + } + else + { + prices.Add(new PriceMwh(dto.Fuels.KerosineEuroPerMWh, item.Name)); + } + } + return prices; + } + + private void CalculatePowerWindTubine(IEnumerable windTurbines, List resultList, FuelInput fuels, int load) + { + if (windTurbines != null) + { + if(fuels.WindPercentage > 0) + { + var sum = resultList.Sum(x => x.P); + foreach (var wind in windTurbines) + { + var maxWindPower = wind.Pmax * (fuels.WindPercentage / 100); + if (load <= sum + maxWindPower) + { + AddProduct(wind.Name, load - sum, resultList); + break; + } + AddProduct(wind.Name, maxWindPower, resultList); + sum = sum + maxWindPower; + } + } + else + { + windTurbines.ToList().ForEach(powerPlant => AddProduct(powerPlant.Name, 0, resultList)); + } + + } + } + private void CalculatePowerNotWindTurbines(ProductPlanInput dto, List resultList, List prices) + { + if (dto.Powerplants != null) + { + bool isNextEmpty = false; + var sum = resultList.Sum(x => x.P); + prices = prices.OrderBy(x => x.Price).ThenBy(x => x.Name).ToList(); + foreach (var price in prices) + { + if (isNextEmpty) + { + AddProduct(price.Name, 0, resultList); + continue; + } + var plant = dto.Powerplants.First(x => x.Name == price.Name && x.Type != Enums.TypePowerEnum.Windturbine); + + if (dto.Load <= sum + plant.Pmax) + { + AddProduct(plant.Name, dto.Load - sum, resultList); + isNextEmpty = true; + continue; + } + AddProduct(plant.Name, plant.Pmax, resultList); + sum = sum + plant.Pmax; + } + } + } + private void AddProduct(string name, decimal price, List resultList) + { + resultList.Add(new ProductResponse() + { + Name = name, + P = price + }); + } + } + +} diff --git a/Powerplants.BusinessLogic/Services/Calculation/ICalculationPowerPlanService.cs b/Powerplants.BusinessLogic/Services/Calculation/ICalculationPowerPlanService.cs new file mode 100644 index 000000000..ac8e94da2 --- /dev/null +++ b/Powerplants.BusinessLogic/Services/Calculation/ICalculationPowerPlanService.cs @@ -0,0 +1,8 @@ +using BusinessLogic.Models; +namespace BusinessLogic.Services.Calculation +{ + public interface ICalculationPowerPlanService + { + List CalculatePowerPlants(ProductPlanInput dto); + } +} diff --git a/Powerplants.UnitTests/CalculationPowerPlanServiceTests.cs b/Powerplants.UnitTests/CalculationPowerPlanServiceTests.cs new file mode 100644 index 000000000..ff0c4a786 --- /dev/null +++ b/Powerplants.UnitTests/CalculationPowerPlanServiceTests.cs @@ -0,0 +1,36 @@ +using BusinessLogic.Services.Calculation; + +namespace PowerPlant.UnitTests +{ + public class CalculationPowerPlanServiceTests + { + private readonly ICalculationPowerPlanService _calculatorPowerPlanService; + + public CalculationPowerPlanServiceTests() + { + _calculatorPowerPlanService = new CalculationPowerPlanService(); + } + [Fact] + public void Response_Should_Equals_Json_Response() + { + var payload = HelperMethods.GetPayload(); + payload.Fuels.GasEuroPerMWh = 13.4m; + payload.Fuels.KerosineEuroPerMWh = 50.8m; + payload.Fuels.Co2EuroPerTon = 20; + payload.Fuels.WindPercentage = 60; + + var responseFromFile = HelperMethods.GetResponse(); + + var result = _calculatorPowerPlanService.CalculatePowerPlants(payload); + Assert.Equal(responseFromFile.Count, result.Count); + + for (int i = 0; i < result.Count; i++) + { + responseFromFile[i].Name = result[i].Name; + responseFromFile[i].P = result[i].P; + } + } + + + } +} \ No newline at end of file diff --git a/Powerplants.UnitTests/HelperMethods.cs b/Powerplants.UnitTests/HelperMethods.cs new file mode 100644 index 000000000..ae0691bc6 --- /dev/null +++ b/Powerplants.UnitTests/HelperMethods.cs @@ -0,0 +1,32 @@ +using BusinessLogic.Models; +using Newtonsoft.Json; + +namespace PowerPlant.UnitTests +{ + internal static class HelperMethods + { + public static List GetResponse() + { + using (StreamReader reader = new StreamReader("response.json")) + using (JsonTextReader jsonReader = new JsonTextReader(reader)) + { + JsonSerializer serializer = new JsonSerializer(); + return serializer.Deserialize>(jsonReader)!; + + } + + } + + public static ProductPlanInput GetPayload() + { + using (StreamReader reader = new StreamReader("payload.json")) + using (JsonTextReader jsonReader = new JsonTextReader(reader)) + { + JsonSerializer serializer = new JsonSerializer(); + return serializer.Deserialize(jsonReader)!; + + } + + } + } +} diff --git a/Powerplants.UnitTests/PowerPlants.UnitTests.csproj b/Powerplants.UnitTests/PowerPlants.UnitTests.csproj new file mode 100644 index 000000000..0012e79cd --- /dev/null +++ b/Powerplants.UnitTests/PowerPlants.UnitTests.csproj @@ -0,0 +1,42 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + Always + + + Always + + + + + + + + + + + + + + + + + + + + diff --git a/Powerplants.UnitTests/ProductControllerUnitTests.cs b/Powerplants.UnitTests/ProductControllerUnitTests.cs new file mode 100644 index 000000000..b64ee4d8f --- /dev/null +++ b/Powerplants.UnitTests/ProductControllerUnitTests.cs @@ -0,0 +1,29 @@ +using BusinessLogic.Services.Calculation; +using Microsoft.AspNetCore.Mvc; +using Powerplant.Controllers; + + +namespace PowerPlant.UnitTests +{ + public class ProductControllerUnitTests + { + private readonly ProductController _controller; + private readonly ICalculationPowerPlanService _calculatorPowerPlanService; + + public ProductControllerUnitTests() + { + _calculatorPowerPlanService = new CalculationPowerPlanService(); + _controller = new ProductController(_calculatorPowerPlanService); + } + [Fact] + public void Type_Response_ShouldBe_Ok() + { + var payload = HelperMethods.GetPayload(); + + IActionResult result = _controller.CalculatePowerPlants(payload); + Assert.IsType(result); + } + + + } +} \ No newline at end of file diff --git a/Powerplants.UnitTests/payload.json b/Powerplants.UnitTests/payload.json new file mode 100644 index 000000000..bd28884ce --- /dev/null +++ b/Powerplants.UnitTests/payload.json @@ -0,0 +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 + } + ] +} diff --git a/Powerplants.UnitTests/response.json b/Powerplants.UnitTests/response.json new file mode 100644 index 000000000..545c3ef1d --- /dev/null +++ b/Powerplants.UnitTests/response.json @@ -0,0 +1,26 @@ +[ + { + "name": "windpark1", + "p": 90.0 + }, + { + "name": "windpark2", + "p": 21.6 + }, + { + "name": "gasfiredbig1", + "p": 460.0 + }, + { + "name": "gasfiredbig2", + "p": 338.4 + }, + { + "name": "gasfiredsomewhatsmaller", + "p": 0.0 + }, + { + "name": "tj1", + "p": 0.0 + } +] \ No newline at end of file diff --git a/README.md b/README.md index 44c93d608..655e2be1a 100644 --- a/README.md +++ b/README.md @@ -1,99 +1,184 @@ -# powerplant-coding-challenge +# Powerplant Production Plan API +## Overview -## Welcome ! +This API calculates the power production plan for a set of power plants to meet a specified load. It determines how much power each power plant needs to produce while taking into account the cost of the energy sources (such as gas and kerosine) and the minimum (Pmin) and maximum (Pmax) production capacity of each power plant. The goal is to meet the power demand efficiently by minimizing costs while adhering to the constraints of each power plant. -Below you can find the description of a coding challenge that we ask people to perform when applying for a job in our team. +## Prerequisites -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. +Before running the project, make sure you have the following installed: -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. +- **[.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/sdk-8.0.403-windows-x64-installer)** +- **[Visual Studio 2022](https://visualstudio.microsoft.com/)** +- **[Docker](https://www.docker.com/products/docker-desktop/)** -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 +## Getting Started -We are the IS team of the 'Short-term Power as-a-Service' (a.k.a. SPaaS) team within [GEM](https://gems.engie.com/). +### 1. **Start project through command line:** -[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) +Open folder where the project is located and run the following command in the command line: +```bash +dotnet restore +``` + +```bash +dotnet run --project Powerplants.Api +``` + +Swagger of the project will be avaible on https://localhost:8888/swagger/index.html + +### 2. **Start project through visual studio:** + +After installing Visual Studio, make sure to install the **ASP.NET and web development** workload. + +Once installed, Open folder where the project is located and ckick on the **Powerplant.sln** file. In the Solution Explorer, right-click on the **Powerplant** project and select **Set as Startup Project**. Then, press **F5** to start the project. + +In either cases swagger of the project will be avaible on https://localhost:8888/swagger/index.html + +### 3. **Start project through Docker:** + +To run the API in a Docker container, follow these steps: + +1. **Build the Docker image:** + + ```bash + docker build -t powerplants-api . + ``` + +2. **Run the Docker container:** + + ```bash + docker run -p "8888:8888" powerplants-api + ``` + + The API will be accessible at `http://localhost:8888`. + +## API Usage + +### Endpoint + +- **Method**: `POST` +- **URL**: `/productionplan` + +The API accepts a request that contains 3 types of data: + +- load: The energy (MWh) that needs to be generated in one hour. +- fuels: Determines the merit order based on fuel costs, guiding which power plants to activate and how much power they deliver + + - gas (euro/MWh): The price of gas per MWh. For example, at €6/MWh with 50% efficiency, generating 1 MWh costs €12. + - kerosine(euro/Mwh): the price of kerosine per MWh. + - co2(euro/ton): Price of emission allowances. + -wind(%): Wind percentage; e.g., a 25% wind availability means a wind turbine with a Pmax of 4 MW generates 1 MWh. + - name : Identifier of the power plant. + - 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: Maximum power output. + - pmin: Minimum power output when activated.. + +### Sample payload + +```json +{ + "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 + } + ] +} +``` + +### Sample Response + +```json +[ + { + "name": "windpark1", + "p": 90 + }, + { + "name": "windpark2", + "p": 21.6 + }, + { + "name": "gasfiredbig1", + "p": 460 + }, + { + "name": "gasfiredbig2", + "p": 338.4 + }, + { + "name": "gasfiredsomewhatsmaller", + "p": 0 + }, + { + "name": "tj1", + "p": 0 + } +] +``` + +### Testing with Swagger + +The API uses **Swagger** for interactive documentation. Once the API is running, you can access the Swagger UI at: + +``` +http(s)://localhost:8888/swagger/index.html +``` + +This interface allows you to explore and test the available endpoint. + +## Testing + +The project includes unit tests using **xUnit**. To run the tests, use: + +```bash +dotnet test +```