diff --git a/simulator/src/simulator-ui.ts b/simulator/src/simulator-ui.ts index 401c22d..90fe576 100644 --- a/simulator/src/simulator-ui.ts +++ b/simulator/src/simulator-ui.ts @@ -1,15 +1,16 @@ interface SimulationParameters { - batteryCapacity: number, - emptyVehicleWeight: number, - driverWeight: number, - additionalWeight: number, - humanPower: number, - averageSpeed: number, - climateZone: string, - dailyDistance: number, - dailyAscendingElevation: number + batteryCapacity: number; + emptyVehicleWeight: number; + driverWeight: number; + additionalWeight: number; + humanPower: number; + speedLimit: number; + climateZone: string; + dailyDistance: number; + dailyAscendingElevation: number; + flatTerrainRatio: number; } function runSimulation(parameters: SimulationParameters): Simulator.SimulationResult { @@ -21,9 +22,9 @@ function runSimulation(parameters: SimulationParameters): Simulator.SimulationRe vehicle.driverWeight= parameters.driverWeight; vehicle.additionalWeight = parameters.additionalWeight; vehicle.humanPower = parameters.humanPower; - vehicle.averageSpeed = parameters.averageSpeed; + vehicle.speedLimit = parameters.speedLimit; let solarIrradiance: number[] = climateData[parameters.climateZone.toLowerCase()]; - let planning = new Simulator.OutingPlanning(parameters.dailyDistance, parameters.dailyAscendingElevation); + let planning = new Simulator.OutingPlanning(parameters.dailyDistance, parameters.dailyAscendingElevation, parameters.flatTerrainRatio); let simulationResult = Simulator.simulate(vehicle, solarIrradiance, planning); //console.log(solarIrradiance); @@ -68,13 +69,15 @@ function initializeSimulator(container: HTMLElement) { driverWeight: Number((container.querySelector('[name=driver-weight]')).value), additionalWeight: Number((container.querySelector('[name=additional-weight]')).value), humanPower: Number((container.querySelector('[name=human-power]')).value), - averageSpeed: Number((container.querySelector('[name=average-speed]')).value), + speedLimit: Number((container.querySelector('[name=speed-limit]')).value), climateZone: (container.querySelector('.zone-selector')).value, dailyDistance: Number((container.querySelector('[name=daily-distance]')).value), dailyAscendingElevation: Number((container.querySelector('[name=daily-elevation]')).value), + flatTerrainRatio: Number((container.querySelector('[name=flat-ratio]')).value) / 100.0, }; let simulationResult = runSimulation(parameters); + console.log(simulationResult); let resultsContainer = container.querySelector('.simulation-results'); @@ -82,10 +85,17 @@ function initializeSimulator(container: HTMLElement) { let totalConsumedGridPower = simulationResult.cumulatedGridRechargeEnergy / simulationResult.vehicle.batteryEfficiency / simulationResult.vehicle.gridTransformerEfficiency; let solarRechargeRatio = Math.round(100*(simulationResult.cumulatedSolarRechargeEnergy/(simulationResult.cumulatedSolarRechargeEnergy + simulationResult.cumulatedGridRechargeEnergy))); + + let dailyDuration = parameters.dailyDistance / simulationResult.averageSpeed; + let dailyDurationHours = Math.floor(dailyDuration); + let dailyDurationMinutes = Math.round((dailyDuration - dailyDurationHours) * 60); + resultsContainer.querySelector('.result-info').innerHTML = `

Il faudra recharger le vhélio sur secteur environ ${simulationResult.gridChargeCount} fois sur l'année.

-

Cela coûtera ${Math.round(totalConsumedGridPower/1000*averageKwhCost*100)/100}€ sur l'année.

-

Le vhélio sera rechargé à ${solarRechargeRatio}% par le soleil, ${100-solarRechargeRatio}% sur secteur.

+

Cela coûtera ${Math.round(totalConsumedGridPower/1000*averageKwhCost*100)/100}€ sur l'année. Le vhélio sera rechargé à ${solarRechargeRatio}% par le soleil, ${100-solarRechargeRatio}% sur secteur.

+


+

Vitesse moyenne : ${Math.round(simulationResult.averageSpeed*10.0)/10.0} km/h (${Math.round(simulationResult.flatTerrainSpeed*10.0)/10.0} km/h sur plat, ${Math.round(simulationResult.uphillSpeed*10.0)/10.0} km/h en côte, ${Math.round(simulationResult.downhillSpeed*10.0)/10.0} km/h en descente)

+

Durée du trajet quotidien : ${dailyDurationHours}h ${dailyDurationMinutes}min. Distance annuelle : ${Math.round(simulationResult.cumulatedDistance)} km.

`; //

${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.

diff --git a/simulator/src/simulator.html b/simulator/src/simulator.html index 325e88e..ecaa543 100644 --- a/simulator/src/simulator.html +++ b/simulator/src/simulator.html @@ -31,7 +31,7 @@

- +

kg @@ -160,12 +160,12 @@

- +

- +

km/h @@ -173,6 +173,22 @@

+ +
+
+ +
+
+
+

+ +

+

+ % +

+
+
+
diff --git a/simulator/src/simulator.ts b/simulator/src/simulator.ts index 8fdb49c..e0e33c4 100644 --- a/simulator/src/simulator.ts +++ b/simulator/src/simulator.ts @@ -1,4 +1,10 @@ namespace Simulator { + interface ConsumptionData { + motorEnergy: number; + humanEnergy: number; + averageSpeed: number; + } + export class Vehicle { batteryCapacity: number; batteryEfficiency: number = 0.9; @@ -12,12 +18,17 @@ namespace Simulator { additionalWeight: number = 0; // additional weight, not counting cyclist and empty vehicle weight, in kg humanPower: number = 100; // W - averageSpeed: number = 20; // average speed in km/h, when the vehicle is moving (this is important, because driver does not provide power when stopped) + speedLimit: number = 20; // average speed in km/h, when the vehicle is moving (this is important, because driver does not provide power when stopped) nominalMotorPower: number = 250; // W assistanceSpeedLimit: number = 25; // km/h - motorConsumption(distance: number, ascendingElevation: number): number { + consumption(distance: number, ascendingElevation: number, inOutConsumption: ConsumptionData) { + if(distance <= 0) + { + return; + } + const g = 9.8; let totalWeight = this.emptyVehicleWeight + this.driverWeight + this.additionalWeight; let potentialEnergy = totalWeight * g * ascendingElevation; // Ep = m*g*h (result in Joules) @@ -25,32 +36,35 @@ namespace Simulator { // empirical measures let baseConsumption = 13; // in Wh/km, when human power is 0 - let maxWeight = 300; // in kg - let additionalConsumptionAtMaxWeight = 5; // in Wh/km (without accounting for ascending elevation, only accelerations and additional friction) + let additionalConsumptionPerKg = 0.01; // in Wh/km per kg of total vehicle weight (additional losses due to increased friction, mostly independent of speed) - let weightRelatedConsumption = MathUtils.clamp(totalWeight * additionalConsumptionAtMaxWeight / maxWeight, 0, additionalConsumptionAtMaxWeight); + let requiredEnergy = Math.max(0, distance * (baseConsumption + totalWeight * additionalConsumptionPerKg) + potentialEnergy); let motorPowerLimit = this.nominalMotorPower; - let tripDuration = (distance * (baseConsumption + weightRelatedConsumption) + potentialEnergy) / (motorPowerLimit + this.humanPower); + let tripDuration = Math.max(0.0001, requiredEnergy / (motorPowerLimit + this.humanPower)); let actualSpeed = distance / tripDuration; - console.log("Max vehicle speed, according to available power: " + (Math.round(actualSpeed*10)/10) + " km/h") + //console.log("Max vehicle speed, according to available power: " + (Math.round(actualSpeed*10)/10) + " km/h") if(actualSpeed > this.assistanceSpeedLimit) { - tripDuration = distance / this.assistanceSpeedLimit - motorPowerLimit = Math.max(0, ((distance * (baseConsumption + weightRelatedConsumption) + potentialEnergy) - tripDuration * this.humanPower) / tripDuration); - tripDuration = (distance * (baseConsumption + weightRelatedConsumption) + potentialEnergy) / (motorPowerLimit + this.humanPower); + let assistTripDuration = distance / this.assistanceSpeedLimit + motorPowerLimit = Math.max(0, requiredEnergy/assistTripDuration - this.humanPower); + if(motorPowerLimit + this.humanPower > 0) + tripDuration = requiredEnergy / (motorPowerLimit + this.humanPower); actualSpeed = distance / tripDuration; - console.log("Vehicle speed clamped by assistance speed limit, motor power limited to: " + Math.round(motorPowerLimit) + " W") + //console.log("Vehicle speed accounting for assistance speed limit: " + (Math.round(actualSpeed*10)/10) + " km/h (motor power limited to: " + Math.round(motorPowerLimit) + " W)") } - if(actualSpeed > this.averageSpeed) { - actualSpeed = this.averageSpeed; + if(actualSpeed > this.speedLimit) { + actualSpeed = this.speedLimit; tripDuration = distance / actualSpeed; + motorPowerLimit = Math.max(0, requiredEnergy/tripDuration - this.humanPower); } - let humanEnergy = tripDuration * this.humanPower; + let humanEnergy = Math.max(requiredEnergy, tripDuration * this.humanPower); - return Math.max(motorPowerLimit * tripDuration, distance * (baseConsumption + weightRelatedConsumption) + potentialEnergy - humanEnergy); + inOutConsumption.motorEnergy += Math.max(motorPowerLimit * tripDuration, requiredEnergy - humanEnergy); + inOutConsumption.humanEnergy += humanEnergy; + inOutConsumption.averageSpeed += actualSpeed; } solarPower(irradiance: number): number { @@ -66,7 +80,7 @@ namespace Simulator { } export class OutingPlanning { - constructor(public dailyDistance: number, public dailyAscendingElevation: number) { + constructor(public dailyDistance: number, public dailyAscendingElevation: number, public flatTerrainRatio: number) { } getOuting(dayOfWeek: number, hourOfDay: number, outing: Outing) { @@ -95,6 +109,14 @@ namespace Simulator { 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. + cumulatedHumanEnergy: number; + + cumulatedDistance: number; + + flatTerrainSpeed: number; + uphillSpeed: number; + downhillSpeed: number; + averageSpeed: number; } export function simulate(vehicle: Vehicle, solarIrradiance: number[], planning: OutingPlanning): SimulationResult { @@ -106,12 +128,45 @@ namespace Simulator { cumulatedGridRechargeEnergy: 0, cumulatedSolarRechargeEnergy: 0, totalProducedSolarEnergy: 0, - cumulatedMotorConsumption: 0 + cumulatedMotorConsumption: 0, + cumulatedHumanEnergy: 0, + + cumulatedDistance: 0, + + flatTerrainSpeed: 0, + uphillSpeed: 0, + downhillSpeed: 0, + averageSpeed: 0 }; let remainingBatteryCharge = vehicle.batteryCapacity; let outing: Outing = { distance: 0, ascendingElevation: 0 }; + let consumption: ConsumptionData = { motorEnergy: 0, humanEnergy: 0, averageSpeed: 0 }; + + let flatTerrainRatio = MathUtils.clamp(planning.flatTerrainRatio, 0.0, 1.0); + if(planning.dailyAscendingElevation <= 0) flatTerrainRatio = 1.0; + + let flatDistance = planning.dailyDistance * flatTerrainRatio; + consumption = { motorEnergy: 0, humanEnergy: 0, averageSpeed: 0 }; + vehicle.consumption(flatDistance, 0, consumption); + result.flatTerrainSpeed = consumption.averageSpeed; + + let uphillDistance = planning.dailyDistance * (1.0 - flatTerrainRatio) * 0.5; + consumption = { motorEnergy: 0, humanEnergy: 0, averageSpeed: 0 }; + vehicle.consumption(uphillDistance, planning.dailyAscendingElevation, consumption); + result.uphillSpeed = consumption.averageSpeed; + + let downhillDistance = planning.dailyDistance * (1.0 - flatTerrainRatio) * 0.5; + consumption = { motorEnergy: 0, humanEnergy: 0, averageSpeed: 0 }; + vehicle.consumption(downhillDistance, -planning.dailyAscendingElevation, consumption); + result.downhillSpeed = consumption.averageSpeed; + + let dailyTripDuration = + (flatDistance > 0 ? flatDistance / result.flatTerrainSpeed : 0) + + (uphillDistance > 0 ? uphillDistance / result.uphillSpeed : 0) + + (downhillDistance > 0 ? downhillDistance / result.downhillSpeed : 0); + result.averageSpeed = planning.dailyDistance / dailyTripDuration; for(let day = 0; day < 365; ++day) { for(let hour = 0; hour < 24; ++hour) { @@ -119,14 +174,20 @@ namespace Simulator { planning.getOuting(day % 7, hour, outing); - let consumption = outing.distance > 0 ? vehicle.motorConsumption(outing.distance, outing.ascendingElevation) : 0; + consumption.motorEnergy = 0; consumption.humanEnergy = 0; consumption.averageSpeed = 0; + + vehicle.consumption(outing.distance * flatTerrainRatio, 0, consumption); + vehicle.consumption(outing.distance * (1.0 - flatTerrainRatio) * 0.5, outing.ascendingElevation, consumption); + vehicle.consumption(outing.distance * (1.0 - flatTerrainRatio) * 0.5, -outing.ascendingElevation, consumption); + result.cumulatedDistance += outing.distance; + 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; + remainingBatteryCharge += solarCharge - consumption.motorEnergy; let fullGridRecharge = false; if(remainingBatteryCharge > vehicle.batteryCapacity) { @@ -142,7 +203,7 @@ namespace Simulator { result.gridChargeCount += 1; } - result.cumulatedMotorConsumption += consumption; + result.cumulatedMotorConsumption += consumption.motorEnergy; result.cumulatedSolarRechargeEnergy += solarCharge; result.batteryLevel[hourIdx] = fullGridRecharge ? 0 : remainingBatteryCharge;