Browse Source

added battery charge graph

adjusted simulation to have the exact same behavior as the python code
master
Youen Toupin 3 years ago
parent
commit
610ce62935
  1. 10
      simulator/src/app.scss
  2. 20
      simulator/src/simulator-core.ts
  3. 8
      simulator/src/simulator.html
  4. 54
      simulator/src/simulator.ts
  5. 2
      simulator/tools/embed.js

10
simulator/src/app.scss

@ -48,3 +48,13 @@ svg g {
svg text { svg text {
pointer-events: none; pointer-events: none;
} }
.battery-charge-graph {
margin-top: 2rem;
}
.battery-charge-graph {
stroke: black;
fill: none;
stroke-width: 1px;
}

20
simulator/src/simulator-core.ts

@ -29,6 +29,8 @@ class Vehicle {
} }
solarPower(irradiance: number): number { solarPower(irradiance: number): number {
// TODO: should decompose climate data in normal radiance (modulated by incident angle) and diffuse irradiance
// TODO: should add a shadowing factor (the panel won't be always exposed to the sun)
return irradiance * this.solarPanelArea * this.solarPanelEfficiency; return irradiance * this.solarPanelArea * this.solarPanelEfficiency;
} }
} }
@ -51,7 +53,7 @@ class OutingPlanning {
} }
else { else {
// other week day // other week day
dailyRatio = hourOfDay == 8 || hourOfDay == 16 ? 0.5 : 0.0; dailyRatio = hourOfDay == 7 || hourOfDay == 15 ? 0.5 : 0.0;
} }
outing.distance = dailyRatio * this.dailyDistance; outing.distance = dailyRatio * this.dailyDistance;
@ -61,6 +63,7 @@ class OutingPlanning {
interface SimulationResult { interface SimulationResult {
batteryLevel: number[]; // Remaining energy in the battery over time (one entry per hour), in Wh batteryLevel: number[]; // Remaining energy in the battery over time (one entry per hour), in Wh
gridChargeCount: number;
cumulatedGridRechargeEnergy: number; // Cumulated energy added to the battery from the power grid, in Wh of battery charge (actual power grid consumption will be slightly higer due to losses) cumulatedGridRechargeEnergy: number; // Cumulated energy added to the battery from the power grid, in Wh of battery charge (actual power grid consumption will be slightly higer due to losses)
cumulatedSolarRechargeEnergy: number; // Cumulated energy added to the battery from the solar panel, in Wh of battery charge (actual generated power is slightly higher due to losses) cumulatedSolarRechargeEnergy: number; // Cumulated energy added to the battery from the solar panel, in Wh of battery charge (actual generated power is slightly higher due to losses)
cumulatedMotorConsumption: number; // Cumulated energy consumed by the motor, in Wh. In this simulation, this is equal to the energy drawn from the battery. cumulatedMotorConsumption: number; // Cumulated energy consumed by the motor, in Wh. In this simulation, this is equal to the energy drawn from the battery.
@ -77,6 +80,7 @@ interface SimulationParameters {
function runSimulation(vehicle: Vehicle, solarIrradiance: number[], planning: OutingPlanning): SimulationResult { function runSimulation(vehicle: Vehicle, solarIrradiance: number[], planning: OutingPlanning): SimulationResult {
let result: SimulationResult = { let result: SimulationResult = {
batteryLevel: [], batteryLevel: [],
gridChargeCount: 0,
cumulatedGridRechargeEnergy: 0, cumulatedGridRechargeEnergy: 0,
cumulatedSolarRechargeEnergy: 0, cumulatedSolarRechargeEnergy: 0,
cumulatedMotorConsumption: 0 cumulatedMotorConsumption: 0
@ -97,15 +101,17 @@ function runSimulation(vehicle: Vehicle, solarIrradiance: number[], planning: Ou
let solarCharge = production * vehicle.batteryEfficiency; let solarCharge = production * vehicle.batteryEfficiency;
// TODO: we should keep a margin because real users will recharge before they reach the bare minimum required for an outing
remainingBatteryCharge += solarCharge - consumption; remainingBatteryCharge += solarCharge - consumption;
if(remainingBatteryCharge > vehicle.batteryCapacity) { if(remainingBatteryCharge > vehicle.batteryCapacity) {
solarCharge -= remainingBatteryCharge - vehicle.batteryCapacity; solarCharge -= remainingBatteryCharge - vehicle.batteryCapacity;
remainingBatteryCharge = vehicle.batteryCapacity; remainingBatteryCharge = vehicle.batteryCapacity;
} }
else if(remainingBatteryCharge < 0) { else if(remainingBatteryCharge <= 0) {
let rechargeEnergy = vehicle.batteryCapacity - remainingBatteryCharge; let rechargeEnergy = vehicle.batteryCapacity - remainingBatteryCharge;
remainingBatteryCharge += rechargeEnergy; remainingBatteryCharge += rechargeEnergy;
result.cumulatedGridRechargeEnergy += rechargeEnergy; result.cumulatedGridRechargeEnergy += rechargeEnergy;
result.gridChargeCount += 1;
} }
result.cumulatedMotorConsumption += consumption; result.cumulatedMotorConsumption += consumption;
@ -118,7 +124,7 @@ function runSimulation(vehicle: Vehicle, solarIrradiance: number[], planning: Ou
return result; return result;
} }
function startSimulation(parameters: SimulationParameters) { function startSimulation(parameters: SimulationParameters): SimulationResult {
let climateData = (<any>window)['climate-zones-data.csv']; let climateData = (<any>window)['climate-zones-data.csv'];
let vehicle = new Vehicle(); let vehicle = new Vehicle();
@ -128,5 +134,13 @@ function startSimulation(parameters: SimulationParameters) {
let planning = new OutingPlanning(parameters.dailyDistance, parameters.dailyAscendingElevation); let planning = new OutingPlanning(parameters.dailyDistance, parameters.dailyAscendingElevation);
let simulationResult = runSimulation(vehicle, solarIrradiance, planning); let simulationResult = runSimulation(vehicle, solarIrradiance, planning);
//console.log(solarIrradiance);
console.log(simulationResult); console.log(simulationResult);
let averageKwhCost = 0.192; // in €/kWh TODO: to verify, this price seems too high
console.log('Grid recharge cost: ' + (Math.round(simulationResult.gridChargeCount*(vehicle.batteryCapacity/1000)*averageKwhCost*100)/100) + '€');
console.log('Solar energy ratio: ' + Math.round(100*(simulationResult.cumulatedMotorConsumption-(simulationResult.gridChargeCount+1)*vehicle.batteryCapacity)/simulationResult.cumulatedMotorConsumption) + '%');
return simulationResult;
} }

8
simulator/src/simulator.html

@ -106,6 +106,12 @@
</div> </div>
</div> </div>
</div> </div>
<div class="simulation-results is-hidden">
<div class="battery-charge-graph">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1000 300"></svg>
</div>
</div>
</div> </div>
</section> </section>
@ -127,7 +133,7 @@
<button class="delete" aria-label="close"></button> <button class="delete" aria-label="close"></button>
</header> </header>
<section class="modal-card-body"> <section class="modal-card-body">
<svg id="zones-map" width="595" height="564" version="1.0"></svg> <svg id="zones-map" xmlns="http://www.w3.org/2000/svg" width="595" height="564" version="1.0"></svg>
</section> </section>
</div> </div>
</div> </div>

54
simulator/src/simulator.ts

@ -11,22 +11,22 @@ document.addEventListener('DOMContentLoaded', function() {
// In order to be able to style SVG elements with CSS, and register events with javascript, we must use inline SVG (we can't use an img tag) // In order to be able to style SVG elements with CSS, and register events with javascript, we must use inline SVG (we can't use an img tag)
// For this purpose, the SVG file contents are embedded in a javascript file // For this purpose, the SVG file contents are embedded in a javascript file
document.getElementById('zones-map').innerHTML = (<any>window)['climate-zones-map.svg']; container.querySelector('#zones-map').innerHTML = (<any>window)['climate-zones-map.svg'];
document.querySelectorAll("[data-activate-modal]").forEach(elt => { container.querySelectorAll("[data-activate-modal]").forEach(elt => {
elt.addEventListener('click', e => { elt.addEventListener('click', e => {
document.getElementById(elt.getAttribute('data-activate-modal')).classList.toggle('is-active', true); container.querySelector('#'+elt.getAttribute('data-activate-modal')).classList.toggle('is-active', true);
}); });
}); });
document.querySelectorAll('.modal-close, .modal-card-head .delete').forEach(elt => { container.querySelectorAll('.modal-close, .modal-card-head .delete').forEach(elt => {
elt.addEventListener('click', e => { elt.addEventListener('click', e => {
closest(elt, e => e.classList.contains('modal')).classList.toggle('is-active', false); closest(elt, e => e.classList.contains('modal')).classList.toggle('is-active', false);
}); });
}); });
let zoneSelector = <HTMLSelectElement>document.getElementById('zone-selector'); let zoneSelector = <HTMLSelectElement>container.querySelector('#zone-selector');
document.querySelectorAll('.climate-zone').forEach(elt => { container.querySelectorAll('.climate-zone').forEach(elt => {
elt.addEventListener('click', e => { elt.addEventListener('click', e => {
let zoneName = elt.getAttribute('id'); let zoneName = elt.getAttribute('id');
zoneSelector.value = zoneName; zoneSelector.value = zoneName;
@ -34,14 +34,42 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
}); });
document.getElementById('simulate-button').addEventListener('click', e => { container.querySelector('#simulate-button').addEventListener('click', e => {
let parameters: SimulationParameters = { let parameters: SimulationParameters = {
batteryCapacity: Number((<HTMLInputElement>document.querySelector('[name=battery-capacity]')).value), batteryCapacity: Number((<HTMLInputElement>container.querySelector('[name=battery-capacity]')).value),
additionalWeight: Number((<HTMLInputElement>document.querySelector('[name=additional-weight]')).value), additionalWeight: Number((<HTMLInputElement>container.querySelector('[name=additional-weight]')).value),
climateZone: (<HTMLSelectElement>document.getElementById('zone-selector')).value, climateZone: (<HTMLSelectElement>container.querySelector('#zone-selector')).value,
dailyDistance: Number((<HTMLInputElement>document.querySelector('[name=daily-distance]')).value), dailyDistance: Number((<HTMLInputElement>container.querySelector('[name=daily-distance]')).value),
dailyAscendingElevation: Number((<HTMLInputElement>document.querySelector('[name=daily-elevation]')).value), dailyAscendingElevation: Number((<HTMLInputElement>container.querySelector('[name=daily-elevation]')).value),
}; };
startSimulation(parameters); let simulationResult = startSimulation(parameters);
let resultsContainer = container.querySelector('.simulation-results');
let batteryChargeGraph = resultsContainer.querySelector('.battery-charge-graph');
let batteryChargeGraphSvg = batteryChargeGraph.querySelector('svg');
let coordinates = '';
let view = [1000, 300];
let hoursInYear = 365 * 24;
for(let dayOfYear = 0; dayOfYear < 365; ++dayOfYear) {
for(let hourOfDay = 0; hourOfDay < 24; ++hourOfDay) {
let h = dayOfYear * 24 + hourOfDay;
let batteryLevel = simulationResult.batteryLevel[h];
if(h == 0) coordinates += 'M';
else if(h == 1) coordinates += ' L';
else coordinates += ' ';
coordinates += Math.round(h * view[0] / hoursInYear)+','+Math.round(view[1] - batteryLevel * view[1] / parameters.batteryCapacity);
}
}
let path = document.createElementNS('http://www.w3.org/2000/svg','path');
path.setAttribute('class','graph');
path.setAttribute('d', coordinates);
path.setAttribute('shape-rendering', 'optimizeQuality')
batteryChargeGraphSvg.append(path);
resultsContainer.classList.toggle('is-hidden', false);
}); });
}); });

2
simulator/tools/embed.js

@ -52,7 +52,7 @@ function embedCsv(src, dst) {
fs.readFile(src, 'utf8', function(err, csvData) { fs.readFile(src, 'utf8', function(err, csvData) {
if(err) throw err; if(err) throw err;
let csvLines = csvData.split('\n').map(str => str.split(';')); let csvLines = csvData.split('\n').map(str => str.replace('\r', '').split(';'));
let jsData = {}; let jsData = {};

Loading…
Cancel
Save