Added quarter based simulation with efficiency and capacity parameters.

This commit is contained in:
2026-04-14 22:20:01 +02:00
parent 3a0647aff2
commit 47d835d336
6 changed files with 147 additions and 80 deletions

View File

@@ -1,14 +0,0 @@
using System;
namespace BattSim.Models
{
public class BatteryDayResult
{
public DateOnly Date { get; set; }
public double ChargedEnergy { get; set; }
public double UsedEnergy { get; set; }
public double RemainingEnergy { get; set; }
public double ReducedConsumption { get; set; }
public double ReducedProduction { get; set; }
}
}

View File

@@ -8,5 +8,14 @@ namespace BattSim.Models
public bool DayTariff { get; set; }
public double Consumption { get; set; }
public double Production { get; set; }
public EnergyData(){}
public EnergyData(EnergyData other)
{
Time = other.Time;
DayTariff = other.DayTariff;
Consumption = other.Consumption;
Production = other.Production;
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
namespace BattSim.Models
{
public class SimulatedBatteryEnergyData : EnergyData
{
public double BatteryCharge { get; set; }
public double SimulatedConsumption { get; set; }
public double SimulatedProduction { get; set; }
public SimulatedBatteryEnergyData(EnergyData energyData) : base(energyData) { }
public SimulatedBatteryEnergyData(SimulatedBatteryEnergyData simulatedBatteryEnergyData) : base(simulatedBatteryEnergyData)
{
BatteryCharge = simulatedBatteryEnergyData.BatteryCharge;
SimulatedConsumption = simulatedBatteryEnergyData.SimulatedConsumption;
SimulatedProduction = simulatedBatteryEnergyData.SimulatedProduction;
}
}
}

View File

@@ -4,32 +4,29 @@
@using BattSim.Models
@using BattSim.Services
<PageTitle>BattSim</PageTitle>
<PageTitle>Energie Simulator</PageTitle>
<h1>BattSim</h1>
<h1>Energie Simulator</h1>
<h2>Input Data</h2>
<h2>Je Fluvius Energie Data</h2>
<div>
<p>Upload your Fluvius quarterly csv file here. The longer the timeframe the longer it takes to process.</p>
<p>Ga naar de website van Fluvius en download je historische elektriciteitsverbruiksgegevens met kwartier nauwkeurigheid. Voor een optimale simulatie is het raadzaam om een volledig jaar te downloaden. Dit geeft je inzicht in seizoensgebonden patronen en helpt om realistische uitkomsten te genereren.</p>
<InputFile OnChange="OnFileUploaded" accept=".csv"/>
@if (_isLoadingFile)
{
<p>Loading...</p>
}
@if (_isLoadingFile) { <p>Data aan het inladen...</p> }
else
{
@if (FluviusDataRaw.Count != 0)
@if (FluviusDataRaw.Length != 0)
{
<p>@(FluviusDataRaw.Count) entries read.</p>
<p>@(FluviusDataRaw.Length) kwartieren ingeladen.</p>
}
@if (FluviusDataDaily.Count != 0)
@if (FluviusDataDaily.Length != 0)
{
<RadzenChart>
<RadzenAreaSeries Smooth=true Data="@FluviusDataDaily.Values" CategoryProperty="Time" Title="Consumption" ValueProperty="Consumption">
<RadzenAreaSeries Smooth=true Data="@FluviusDataDaily" CategoryProperty="Time" Title="Consumption" ValueProperty="Consumption">
<RadzenChartTooltipOptions Visible="true"/>
</RadzenAreaSeries>
<RadzenAreaSeries Smooth=true Data="@FluviusDataDaily.Values" CategoryProperty="Time" Title="Production" ValueProperty="Production">
<RadzenAreaSeries Smooth=true Data="@FluviusDataDaily" CategoryProperty="Time" Title="Production" ValueProperty="Production">
<RadzenChartTooltipOptions Visible="true"/>
</RadzenAreaSeries>
<RadzenCategoryAxis Formatter="@FormatObject" Padding="20" LabelAutoRotation="-45">
@@ -46,8 +43,8 @@
</div>
@code{
Dictionary<DateTime, EnergyData> FluviusDataRaw = [];
Dictionary<DateOnly, EnergyData> FluviusDataDaily = [];
EnergyData[] FluviusDataRaw = [];
EnergyData[] FluviusDataDaily = [];
bool _isLoadingFile = false;
private async Task OnFileUploaded(InputFileChangeEventArgs e)
@@ -79,41 +76,75 @@
private void OnSeriesClick(){}
private string FormatObject(object value) {
if(value is double d) return $"{d:0.##} kWh";
if(value is DateTime date) return date.ToString("dd/MM/yyyy");
if(value is DateTime time) return time.ToString("dd/MM/yyyy");
if(value is DateOnly date) return date.ToString("dd/MM/yyyy");
return string.Empty;
}
}
<h2>Simulate Battery</h2>
<h2>Batterij Simulatie</h2>
<div>
<p>Generate adjusted energy data simulating the effect of a battery with properties as configured here:</p>
<p>
Met je Fluvius-kwartierdata simuleren we een batterij die elk kwartier de energie die normaal op het elektriciteitsnet wordt teruggeleverd, opvangt en de stroom die normaal uit het net wordt gehaald vervangt—mits de batterij nog voldoende capaciteit heeft.
Op deze manier kun je verschillende batterijcapaciteiten testen om te zien welke installatie het beste past bij jouw verbruiksprofiel en besparingsdoelen. Daarnaast kun je onderzoeken hoe verschillende batterijtypes, met uiteenlopende efficiëntieniveaus, de uitkomsten beïnvloeden. Onder efficiëntie verstaan we hier het aandeel van de opgeslagen energie dat uiteindelijk ook daadwerkelijk weer kan worden gebruikt.
</p>
<ul>
<li>
<p>Capacity: <InputNumber @bind-value="BatteryCapacity"/> kWh</p>
<p>Batterijcapaciteit: <InputNumber @bind-value="BatteryCapacity"/> kWh</p>
</li>
<li>
<p>Discharge Rate: <InputNumber @bind-value="DischargeRate"/> kW</p>
</li>
<li>
<p>Charge Rate: <InputNumber @bind-value="ChargeRate"/> kW</p>
</li>
<li>
<p>Round-trip Efficiency: <InputNumber @bind-value="Efficiency"/> %</p>
<p>Round-trip Efficiëntie: <InputNumber @bind-value="Efficiency"/> %</p>
</li>
</ul>
<Button @onclick="SimulateBattery">Simulate</Button>
@if (_isSimulating) { <p>Simulating...</p> }
else
{
@if (SimulationDataDaily.Length != 0)
{
<RadzenChart>
<RadzenAreaSeries Smooth=true Data="@SimulationDataDaily" CategoryProperty="Time" Title="Consumption" ValueProperty="Consumption">
<RadzenChartTooltipOptions Visible="true"/>
</RadzenAreaSeries>
<RadzenAreaSeries Smooth=true Data="@SimulationDataDaily" CategoryProperty="Time" Title="Production" ValueProperty="Production">
<RadzenChartTooltipOptions Visible="true"/>
</RadzenAreaSeries>
<RadzenAreaSeries Smooth=true Data="@SimulationDataDaily" CategoryProperty="Time" Title="SimulatedConsumption" ValueProperty="SimulatedConsumption">
<RadzenChartTooltipOptions Visible="true"/>
</RadzenAreaSeries>
<RadzenAreaSeries Smooth=true Data="@SimulationDataDaily" CategoryProperty="Time" Title="SimulatedProduction" ValueProperty="SimulatedProduction">
<RadzenChartTooltipOptions Visible="true"/>
</RadzenAreaSeries>
<RadzenAreaSeries Smooth=true Data="@SimulationDataDaily" CategoryProperty="Time" Title="BatteryCharge" ValueProperty="BatteryCharge">
<RadzenChartTooltipOptions Visible="true"/>
</RadzenAreaSeries>
<RadzenCategoryAxis Formatter="@FormatObject" Padding="20" LabelAutoRotation="-45">
<RadzenGridLines Visible="true"/>
<RadzenAxisTitle Text="Time"/>
</RadzenCategoryAxis>
<RadzenValueAxis Formatter="@FormatObject">
<RadzenGridLines Visible="true"/>
<RadzenAxisTitle Text="Energy"/>
</RadzenValueAxis>
</RadzenChart>
}
}
</div>
@code {
double BatteryCapacity = 7.5;
double DischargeRate = 3;
double ChargeRate = 3;
double Efficiency = 90;
BatteryDayResult[] SimulationData = [];
bool _isSimulating = false;
private async Task SimulateBattery(){
SimulatedBatteryEnergyData[] SimulationData = [];
SimulatedBatteryEnergyData[] SimulationDataDaily = [];
private void SimulateBattery(){
Console.WriteLine("Simulating...");
// SimulationData = BatterySimulator.SimulateBattery(EnergyData, BatteryCapacity).ToArray();
_isSimulating = true;
SimulationData = BatterySimulator.SimulateBattery(FluviusDataRaw, BatteryCapacity, Efficiency/100);
SimulationDataDaily = BatterySimulator.GenerateDailyData(SimulationData);
_isSimulating = false;
Console.WriteLine("Done simulating!");
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using BattSim.Models;
@@ -5,39 +6,59 @@ namespace BattSim.Services
{
public static class BatterySimulator
{
public static List<BatteryDayResult> SimulateBattery(EnergyData[] data, double batteryCapacity)
public static SimulatedBatteryEnergyData[] SimulateBattery(EnergyData[] energyData, double batteryCapacity, double efficiency)
{
var results = new List<BatteryDayResult>();
double remainingEnergy = 0;
var results = new List<SimulatedBatteryEnergyData>();
double batteryCharge = 0;
foreach (var day in data)
foreach (var e in energyData)
{
// // Charge battery from production
// var chargedEnergy = System.Math.Min(day.TotalProduction, batteryCapacity);
// var excessProduction = day.TotalProduction - chargedEnergy;
var simulatedBatteryEnergyData = new SimulatedBatteryEnergyData(e);
// Simulate charging the battery based on production
double excessProduction = batteryCharge + e.Production - batteryCapacity;
batteryCharge = double.Min(batteryCharge + e.Production, batteryCapacity);
simulatedBatteryEnergyData.SimulatedProduction = double.Max(excessProduction, 0);
// // Use battery for consumption
// var usedEnergy = System.Math.Min(chargedEnergy + remainingEnergy, day.TotalConsumption);
// var remainingAfterUse = chargedEnergy + remainingEnergy - usedEnergy;
// // Calculate reduced values
// var reducedConsumption = System.Math.Min(usedEnergy, day.TotalConsumption);
// var reducedProduction = day.TotalProduction - chargedEnergy;
// results.Add(new BatteryDayResult
// {
// Date = day.Date,
// ChargedEnergy = chargedEnergy,
// UsedEnergy = usedEnergy,
// RemainingEnergy = remainingAfterUse,
// ReducedConsumption = reducedConsumption,
// ReducedProduction = reducedProduction
// });
// remainingEnergy = remainingAfterUse;
// Simulate discharging the battery based on consumption
double availableEnergy = batteryCharge * efficiency;
double deficit = e.Consumption - availableEnergy;
double energyDrawn = Math.Min(e.Consumption, availableEnergy);
batteryCharge = batteryCharge - (energyDrawn / efficiency); // Adjust for loss
simulatedBatteryEnergyData.SimulatedConsumption = Math.Max(deficit, 0);
// Register the current charge
simulatedBatteryEnergyData.BatteryCharge = batteryCharge;
results.Add(simulatedBatteryEnergyData);
}
return results;
return results.ToArray();
}
public static SimulatedBatteryEnergyData[] GenerateDailyData(SimulatedBatteryEnergyData[] simulationData)
{
var simulationDataCopy = (SimulatedBatteryEnergyData[]) simulationData.Clone();
var dailySimulationData = new ConcurrentDictionary<DateOnly, SimulatedBatteryEnergyData>();
Parallel.ForEach(simulationDataCopy, simulationPoint =>
{
var date = DateOnly.FromDateTime(simulationPoint.Time);
// Use AddOrUpdate to avoid double lookup
dailySimulationData.AddOrUpdate(
date,
new SimulatedBatteryEnergyData(simulationPoint), // If key doesn't exist, add this value
(_, existing) =>
{
// If key exists, aggregate the values
existing.Consumption += simulationPoint.Consumption;
existing.Production += simulationPoint.Production;
existing.SimulatedConsumption += simulationPoint.SimulatedConsumption;
existing.SimulatedProduction += simulationPoint.SimulatedProduction;
existing.BatteryCharge = double.Max(existing.BatteryCharge, simulationPoint.BatteryCharge);
return existing;
}
);
});
return dailySimulationData.Values.ToArray();
}
}
}

View File

@@ -13,7 +13,7 @@ namespace BattSim.Services
{
public static class FluviusDataHandler
{
public static async Task<Dictionary<DateTime, EnergyData>> LoadAndProcessFile(IBrowserFile file)
public static async Task<EnergyData[]> LoadAndProcessFile(IBrowserFile file)
{
var lines = await ReadCsvFile(file);
var energyData = new ConcurrentDictionary<DateTime,EnergyData>(); // Thread-safe collection
@@ -48,21 +48,22 @@ namespace BattSim.Services
Console.WriteLine($"Error parsing line: {line}. Skipping to next line. Exception: {e}");
}
});
return energyData.ToDictionary();
var results = energyData.Values.ToArray();
results.Sort((a,b)=>a.Time.CompareTo(b.Time));
return results;
}
public static Dictionary<DateOnly, EnergyData> GenerateDailyData(Dictionary<DateTime, EnergyData> energyData)
public static EnergyData[] GenerateDailyData(EnergyData[] energyData)
{
var dailyEnergyData = new ConcurrentDictionary<DateOnly, EnergyData>();
Parallel.ForEach(energyData, entry =>
Parallel.ForEach(energyData, energy =>
{
var date = DateOnly.FromDateTime(entry.Key);
var energy = entry.Value;
var date = DateOnly.FromDateTime(energy.Time);
// Use AddOrUpdate to avoid double lookup
dailyEnergyData.AddOrUpdate(
date,
energy, // If key doesn't exist, add this value
new EnergyData(energy), // If key doesn't exist, add this value
(_, existing) =>
{
// If key exists, aggregate the values
@@ -72,7 +73,7 @@ namespace BattSim.Services
}
);
});
return dailyEnergyData.ToDictionary();
return dailyEnergyData.Values.ToArray();
}
private static async Task<List<string>> ReadCsvFile(IBrowserFile file)