diff --git a/simulator/src/app.scss b/simulator/src/app.scss index 5bfdfc5..fdadf5b 100644 --- a/simulator/src/app.scss +++ b/simulator/src/app.scss @@ -48,3 +48,13 @@ svg g { svg text { pointer-events: none; } + +.battery-charge-graph { + margin-top: 2rem; +} + +.battery-charge-graph { + stroke: black; + fill: none; + stroke-width: 1px; +} diff --git a/simulator/src/simulator-core.ts b/simulator/src/simulator-core.ts index 638ee7f..86b6e28 100644 --- a/simulator/src/simulator-core.ts +++ b/simulator/src/simulator-core.ts @@ -29,6 +29,8 @@ class Vehicle { } 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; } } @@ -51,7 +53,7 @@ class OutingPlanning { } else { // 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; @@ -61,6 +63,7 @@ class OutingPlanning { interface SimulationResult { 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) 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 { let result: SimulationResult = { batteryLevel: [], + gridChargeCount: 0, cumulatedGridRechargeEnergy: 0, cumulatedSolarRechargeEnergy: 0, cumulatedMotorConsumption: 0 @@ -97,15 +101,17 @@ function runSimulation(vehicle: Vehicle, solarIrradiance: number[], planning: Ou 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; if(remainingBatteryCharge > vehicle.batteryCapacity) { solarCharge -= remainingBatteryCharge - vehicle.batteryCapacity; remainingBatteryCharge = vehicle.batteryCapacity; } - else if(remainingBatteryCharge < 0) { + else if(remainingBatteryCharge <= 0) { let rechargeEnergy = vehicle.batteryCapacity - remainingBatteryCharge; remainingBatteryCharge += rechargeEnergy; result.cumulatedGridRechargeEnergy += rechargeEnergy; + result.gridChargeCount += 1; } result.cumulatedMotorConsumption += consumption; @@ -118,7 +124,7 @@ function runSimulation(vehicle: Vehicle, solarIrradiance: number[], planning: Ou return result; } -function startSimulation(parameters: SimulationParameters) { +function startSimulation(parameters: SimulationParameters): SimulationResult { let climateData = (window)['climate-zones-data.csv']; let vehicle = new Vehicle(); @@ -128,5 +134,13 @@ function startSimulation(parameters: SimulationParameters) { let planning = new OutingPlanning(parameters.dailyDistance, parameters.dailyAscendingElevation); let simulationResult = runSimulation(vehicle, solarIrradiance, planning); + //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; } diff --git a/simulator/src/simulator.html b/simulator/src/simulator.html index 605ba33..779a80e 100644 --- a/simulator/src/simulator.html +++ b/simulator/src/simulator.html @@ -106,6 +106,12 @@ + + @@ -127,7 +133,7 @@ \ No newline at end of file diff --git a/simulator/src/simulator.ts b/simulator/src/simulator.ts index d3c24ed..c758a29 100644 --- a/simulator/src/simulator.ts +++ b/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) // For this purpose, the SVG file contents are embedded in a javascript file - document.getElementById('zones-map').innerHTML = (window)['climate-zones-map.svg']; + container.querySelector('#zones-map').innerHTML = (window)['climate-zones-map.svg']; - document.querySelectorAll("[data-activate-modal]").forEach(elt => { + container.querySelectorAll("[data-activate-modal]").forEach(elt => { 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 => { closest(elt, e => e.classList.contains('modal')).classList.toggle('is-active', false); }); }); - let zoneSelector = document.getElementById('zone-selector'); - document.querySelectorAll('.climate-zone').forEach(elt => { + let zoneSelector = container.querySelector('#zone-selector'); + container.querySelectorAll('.climate-zone').forEach(elt => { elt.addEventListener('click', e => { let zoneName = elt.getAttribute('id'); 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 = { - batteryCapacity: Number((document.querySelector('[name=battery-capacity]')).value), - additionalWeight: Number((document.querySelector('[name=additional-weight]')).value), - climateZone: (document.getElementById('zone-selector')).value, - dailyDistance: Number((document.querySelector('[name=daily-distance]')).value), - dailyAscendingElevation: Number((document.querySelector('[name=daily-elevation]')).value), + batteryCapacity: Number((container.querySelector('[name=battery-capacity]')).value), + additionalWeight: Number((container.querySelector('[name=additional-weight]')).value), + climateZone: (container.querySelector('#zone-selector')).value, + dailyDistance: Number((container.querySelector('[name=daily-distance]')).value), + dailyAscendingElevation: Number((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); }); }); diff --git a/simulator/tools/embed.js b/simulator/tools/embed.js index d4ad304..a9078ff 100644 --- a/simulator/tools/embed.js +++ b/simulator/tools/embed.js @@ -52,7 +52,7 @@ function embedCsv(src, dst) { fs.readFile(src, 'utf8', function(err, csvData) { 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 = {};