Compare commits

...

2 Commits

Author SHA1 Message Date
1b39d85cb5 using HTML classes instead of ids to allow multiple simulators 2021-10-15 21:35:13 +02:00
54a9d49d83 Improved statistics display
- fixed bug in solar recharge percentage computation
- added recharge at the end of the year to have the same battery level
at the start and end of the year
- added battery and charger efficiency simulation
- using kWh price (without counting subscription)
2021-10-15 21:24:58 +02:00
3 changed files with 40 additions and 30 deletions

View File

@ -21,30 +21,20 @@ function runSimulation(parameters: SimulationParameters): Simulator.SimulationRe
//console.log(solarIrradiance);
//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;
}
document.addEventListener('DOMContentLoaded', function() {
// Load CSS
document.getElementsByTagName('head')[0].innerHTML += (<any>window)['simulator.css'];
let container = document.getElementById('simulator');
function initializeSimulator(container: HTMLElement) {
// Insert HTML code in the container
container.innerHTML += (<any>window)['simulator.html'];
// 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
container.querySelector('#zones-map').innerHTML = (<any>window)['climate-zones-map.svg'];
container.querySelector('.zones-map').innerHTML = (<any>window)['climate-zones-map.svg'];
container.querySelectorAll("[data-activate-modal]").forEach(elt => {
elt.addEventListener('click', e => {
container.querySelector('#'+elt.getAttribute('data-activate-modal')).classList.toggle('is-active', true);
container.querySelector('.'+elt.getAttribute('data-activate-modal')).classList.toggle('is-active', true);
});
});
@ -54,7 +44,7 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
let zoneSelector = <HTMLSelectElement>container.querySelector('#zone-selector');
let zoneSelector = <HTMLSelectElement>container.querySelector('.zone-selector');
container.querySelectorAll('.climate-zone').forEach(elt => {
elt.addEventListener('click', e => {
let zoneName = elt.getAttribute('id');
@ -63,11 +53,11 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
container.querySelector('#simulate-button').addEventListener('click', e => {
container.querySelector('.simulate-button').addEventListener('click', e => {
let parameters: SimulationParameters = {
batteryCapacity: Number((<HTMLInputElement>container.querySelector('[name=battery-capacity]')).value),
additionalWeight: Number((<HTMLInputElement>container.querySelector('[name=additional-weight]')).value),
climateZone: (<HTMLSelectElement>container.querySelector('#zone-selector')).value,
climateZone: (<HTMLSelectElement>container.querySelector('.zone-selector')).value,
dailyDistance: Number((<HTMLInputElement>container.querySelector('[name=daily-distance]')).value),
dailyAscendingElevation: Number((<HTMLInputElement>container.querySelector('[name=daily-elevation]')).value),
};
@ -76,12 +66,16 @@ document.addEventListener('DOMContentLoaded', function() {
let resultsContainer = container.querySelector('.simulation-results');
let averageKwhCost = 0.192; // in €/kWh TODO: to verify, this price seems too high
let averageKwhCost = 0.1558; // in €/kWh
let totalConsumedGridPower = simulationResult.cumulatedGridRechargeEnergy / simulationResult.vehicle.batteryEfficiency / simulationResult.vehicle.gridTransformerEfficiency;
let solarRechargeRatio = Math.round(100*(simulationResult.cumulatedSolarRechargeEnergy/(simulationResult.cumulatedSolarRechargeEnergy + simulationResult.cumulatedGridRechargeEnergy)));
resultsContainer.querySelector('.result-info').innerHTML = `
<p>Il faudra recharger le vhélio sur secteur environ ${simulationResult.gridChargeCount} fois sur l'année</p>
<p>Cela coûtera ${Math.round(simulationResult.gridChargeCount*(parameters.batteryCapacity/1000)*averageKwhCost*100)/100} sur l'année</p>
<p>La couverture solaire du vhélio est de ${Math.round(100*(simulationResult.cumulatedMotorConsumption-(simulationResult.gridChargeCount+1)*parameters.batteryCapacity)/simulationResult.cumulatedMotorConsumption)}%</p>
<p>Il faudra recharger le vhélio sur secteur environ ${simulationResult.gridChargeCount} fois sur l'année.</p>
<p>Cela coûtera ${Math.round(totalConsumedGridPower/1000*averageKwhCost*100)/100} sur l'année.</p>
<p>Le vhélio sera rechargé à ${solarRechargeRatio}% par le soleil, ${100-solarRechargeRatio}% sur secteur.</p>
`;
//<p>${Math.round(100*(simulationResult.cumulatedSolarRechargeEnergy/simulationResult.vehicle.batteryEfficiency) / simulationResult.totalProducedSolarEnergy)}% de l'énergie produite par le panneau photovoltaïque sera utilisée pour recharger le vhélio.</p>
let batteryChargeGraph = new SvgDrawing.SvgElement(resultsContainer.querySelector('.battery-charge-graph svg'));
@ -100,7 +94,7 @@ document.addEventListener('DOMContentLoaded', function() {
batteryChargeGraph.viewport.setData({ x: 0, y: 0, width: 365*24, height: parameters.batteryCapacity });
batteryChargeGraph.viewport.setView({ x: marginLeft, y: batteryChargeGraph.height - marginBottom, width: batteryChargeGraph.width - (marginLeft+marginRight), height: -batteryChargeGraph.height+(marginTop+marginBottom) });
batteryChargeGraph.graph(simulationResult.batteryLevel, simulationResult.batteryLevel.map(x => x == 0 ? 1 : 0), [{className: ''}, {className: 'grid-recharge'}]);
batteryChargeGraph.graph(simulationResult.batteryLevel, simulationResult.batteryLevel.map((x, idx) => x == 0 || idx == simulationResult.batteryLevel.length - 2 ? 1 : 0), [{className: ''}, {className: 'grid-recharge'}]);
let months = ['Jan', 'Fev', 'Mar', 'Avr', 'Mai', 'Jui', 'Jui', 'Aou', 'Sep', 'Oct', 'Nov', 'Dec'];
let monthWidth = 365*24/12
for(let month = 0; month < 12; ++month) {
@ -115,4 +109,12 @@ document.addEventListener('DOMContentLoaded', function() {
resultsContainer.classList.toggle('is-hidden', false);
});
}
document.addEventListener('DOMContentLoaded', function() {
// Load CSS
document.getElementsByTagName('head')[0].innerHTML += (<any>window)['simulator.css'];
let container = document.getElementById('simulator');
initializeSimulator(container);
});

View File

@ -42,7 +42,7 @@
<div class="field has-addons">
<div class="control is-expanded">
<div class="select is-fullwidth">
<select id="zone-selector">
<select class="zone-selector">
<option>H1a</option>
<option>H1b</option>
<option>H1c</option>
@ -99,7 +99,7 @@
<div class="field-body">
<div class="field">
<p class="control is-expanded">
<a id="simulate-button" class="button is-primary is-fullwidth">Simuler</a>
<a class="simulate-button button is-primary is-fullwidth">Simuler</a>
</p>
</div>
</div>
@ -126,7 +126,7 @@
</div>
</footer>
<div id="zones-map-modal" class="modal">
<div class="modal zones-map-modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
@ -134,7 +134,7 @@
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<svg id="zones-map" xmlns="http://www.w3.org/2000/svg" width="595" height="564" version="1.0"></svg>
<svg class="zones-map" xmlns="http://www.w3.org/2000/svg" width="595" height="564" version="1.0"></svg>
</section>
</div>
</div>

View File

@ -1,7 +1,8 @@
namespace Simulator {
export class Vehicle {
batteryCapacity: number;
batteryEfficiency: number = 1.0; // TODO: typical efficiency of a Li-ion battery (round-trip) is 90%
batteryEfficiency: number = 0.9;
gridTransformerEfficiency: number = 0.85;
solarPanelEfficiency: number = 0.15;
solarPanelArea: number = 1.0; // in square meters
@ -59,19 +60,25 @@ namespace Simulator {
}
export interface SimulationResult {
vehicle: Vehicle;
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)
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)
totalProducedSolarEnergy: number; // Cumulated energy produced (used or unused), before accounting for the battery recharge efficiency.
cumulatedMotorConsumption: number; // Cumulated energy consumed by the motor, in Wh. In this simulation, this is equal to the energy drawn from the battery.
}
export function simulate(vehicle: Vehicle, solarIrradiance: number[], planning: OutingPlanning): SimulationResult {
let result: SimulationResult = {
vehicle: vehicle,
batteryLevel: [],
gridChargeCount: 0,
cumulatedGridRechargeEnergy: 0,
cumulatedSolarRechargeEnergy: 0,
totalProducedSolarEnergy: 0,
cumulatedMotorConsumption: 0
};
@ -88,19 +95,20 @@ namespace Simulator {
let consumption = vehicle.motorConsumption(outing.distance, outing.ascendingElevation);
let production = vehicle.solarPower(solarIrradiance[hourIdx]) * 1.0; // produced energy in Wh is equal to power (W) multiplied by time (h)
result.totalProducedSolarEnergy += production;
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;
let gridRecharge = false;
let fullGridRecharge = false;
if(remainingBatteryCharge > vehicle.batteryCapacity) {
solarCharge -= remainingBatteryCharge - vehicle.batteryCapacity;
remainingBatteryCharge = vehicle.batteryCapacity;
}
else if(remainingBatteryCharge <= 0) {
else if(remainingBatteryCharge <= 0 || (day==364 && hour==23)) {
// TODO: detect if battery capacity is too low for a single outing, abort simulation and display an explanation for the user
gridRecharge = true;
fullGridRecharge = remainingBatteryCharge <= 0;
let rechargeEnergy = vehicle.batteryCapacity - remainingBatteryCharge;
remainingBatteryCharge += rechargeEnergy;
result.cumulatedGridRechargeEnergy += rechargeEnergy;
@ -110,7 +118,7 @@ namespace Simulator {
result.cumulatedMotorConsumption += consumption;
result.cumulatedSolarRechargeEnergy += solarCharge;
result.batteryLevel[hourIdx] = gridRecharge ? 0 : remainingBatteryCharge;
result.batteryLevel[hourIdx] = fullGridRecharge ? 0 : remainingBatteryCharge;
}
}