added battery charge graph
adjusted simulation to have the exact same behavior as the python code
This commit is contained in:
parent
3bc782c9d6
commit
610ce62935
@ -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;
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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…
Reference in New Issue
Block a user