namespace Simulator {
export class Vehicle {
batteryCapacity : number ;
batteryEfficiency : number = 0.9 ;
gridTransformerEfficiency : number = 0.85 ;
solarPanelEfficiency : number = 0.15 ;
solarPanelArea : number = 1.0 ; // in square meters
emptyVehicleWeight : number = 80 ; // kg
driverWeight : number = 60 ; // kg
additionalWeight : number ; // additional weight, not counting cyclist and empty vehicle weight, in kg
motorConsumption ( distance : number , ascendingElevation : number ) : number {
const g = 9.8 ;
let totalWeight = this . emptyVehicleWeight + this . driverWeight + this . additionalWeight ;
let potentialEnergy = totalWeight * g * ascendingElevation ; // Ep = m*g*h (result in Joules)
potentialEnergy = potentialEnergy / 3600 ; // convert joules to watt-hour
// empirical measures
let baseConsumption = 13 ; // in Wh/km
let maxWeight = 300 ; // in kg
let additionalConsumptionAtMaxWeight = 5 ; // in Wh/km (without accounting for ascending elevation, only accelerations and additional friction)
let weightRelatedConsumption = MathUtils . clamp ( totalWeight * additionalConsumptionAtMaxWeight / maxWeight , 0 , additionalConsumptionAtMaxWeight ) ;
return distance * ( baseConsumption + weightRelatedConsumption ) + potentialEnergy ;
}
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 ;
}
}
export interface Outing {
distance : number ; // in km
ascendingElevation : number ; // in meters
}
export class OutingPlanning {
constructor ( public dailyDistance : number , public dailyAscendingElevation : number ) {
}
getOuting ( dayOfWeek : number , hourOfDay : number , outing : Outing ) {
let dailyRatio = 0 ;
if ( dayOfWeek >= 5 ) {
// week end
dailyRatio = hourOfDay == 10 ? 1.0 : 0.0 ;
}
else {
// other week day
dailyRatio = hourOfDay == 7 || hourOfDay == 15 ? 0.5 : 0.0 ;
}
outing . distance = dailyRatio * this . dailyDistance ;
outing . ascendingElevation = dailyRatio * this . dailyAscendingElevation ;
}
}
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
} ;
let remainingBatteryCharge = vehicle . batteryCapacity ;
let outing : Outing = { distance : 0 , ascendingElevation : 0 } ;
for ( let day = 0 ; day < 365 ; ++ day ) {
for ( let hour = 0 ; hour < 24 ; ++ hour ) {
let hourIdx = day * 24 + hour ;
planning . getOuting ( day % 7 , hour , outing ) ;
let consumption = outing . distance > 0 ? vehicle . motorConsumption ( outing . distance , outing . ascendingElevation ) : 0 ;
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 fullGridRecharge = false ;
if ( remainingBatteryCharge > vehicle . batteryCapacity ) {
solarCharge -= remainingBatteryCharge - vehicle . batteryCapacity ;
remainingBatteryCharge = vehicle . batteryCapacity ;
}
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
fullGridRecharge = remainingBatteryCharge <= 0 ;
let rechargeEnergy = vehicle . batteryCapacity - remainingBatteryCharge ;
remainingBatteryCharge += rechargeEnergy ;
result . cumulatedGridRechargeEnergy += rechargeEnergy ;
result . gridChargeCount += 1 ;
}
result . cumulatedMotorConsumption += consumption ;
result . cumulatedSolarRechargeEnergy += solarCharge ;
result . batteryLevel [ hourIdx ] = fullGridRecharge ? 0 : remainingBatteryCharge ;
}
}
return result ;
}
}