@ -1,4 +1,10 @@
namespace Simulator {
namespace Simulator {
interface ConsumptionData {
motorEnergy : number ;
humanEnergy : number ;
averageSpeed : number ;
}
export class Vehicle {
export class Vehicle {
batteryCapacity : number ;
batteryCapacity : number ;
batteryEfficiency : number = 0.9 ;
batteryEfficiency : number = 0.9 ;
@ -12,12 +18,17 @@ namespace Simulator {
additionalWeight : number = 0 ; // additional weight, not counting cyclist and empty vehicle weight, in kg
additionalWeight : number = 0 ; // additional weight, not counting cyclist and empty vehicle weight, in kg
humanPower : number = 100 ; // W
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
nominalMotorPower : number = 250 ; // W
assistanceSpeedLimit : number = 25 ; // km/h
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 ;
const g = 9.8 ;
let totalWeight = this . emptyVehicleWeight + this . driverWeight + this . additionalWeight ;
let totalWeight = this . emptyVehicleWeight + this . driverWeight + this . additionalWeight ;
let potentialEnergy = totalWeight * g * ascendingElevation ; // Ep = m*g*h (result in Joules)
let potentialEnergy = totalWeight * g * ascendingElevation ; // Ep = m*g*h (result in Joules)
@ -25,32 +36,35 @@ namespace Simulator {
// empirical measures
// empirical measures
let baseConsumption = 13 ; // in Wh/km, when human power is 0
let baseConsumption = 13 ; // in Wh/km, when human power is 0
let maxWeight = 300 ; // in kg
let additionalConsumptionPerKg = 0.01 ; // in Wh/km per kg of total vehicle weight (additional losses due to increased friction, mostly independent of speed)
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 ) ;
let requiredEnergy = Math . max ( 0 , distance * ( baseConsumption + totalWeight * additionalConsumptionPerKg ) + potentialEnergy ) ;
let motorPowerLimit = this . nominalMotorPower ;
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 ;
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 ) {
if ( actualSpeed > this . assistanceSpeedLimit ) {
tripDuration = distance / this . assistanceSpeedLimit
let assistTripDuration = distance / this . assistanceSpeedLimit
motorPowerLimit = Math . max ( 0 , ( ( distance * ( baseConsumption + weightRelatedConsumption ) + potentialEnergy ) - tripDuration * this . humanPower ) / tripDuration ) ;
motorPowerLimit = Math . max ( 0 , requiredEnergy / assistTripDuration - this . humanPower ) ;
tripDuration = ( distance * ( baseConsumption + weightRelatedConsumption ) + potentialEnergy ) / ( motorPowerLimit + this . humanPower ) ;
if ( motorPowerLimit + this . humanPower > 0 )
tripDuration = requiredEnergy / ( motorPowerLimit + this . humanPower ) ;
actualSpeed = distance / tripDuration ;
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 ) {
if ( actualSpeed > this . speedLimit ) {
actualSpeed = this . averageSpeed ;
actualSpeed = this . speedLimit ;
tripDuration = distance / actualSpeed ;
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 {
solarPower ( irradiance : number ) : number {
@ -66,7 +80,7 @@ namespace Simulator {
}
}
export class OutingPlanning {
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 ) {
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)
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.
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.
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 {
export function simulate ( vehicle : Vehicle , solarIrradiance : number [ ] , planning : OutingPlanning ) : SimulationResult {
@ -106,12 +128,45 @@ namespace Simulator {
cumulatedGridRechargeEnergy : 0 ,
cumulatedGridRechargeEnergy : 0 ,
cumulatedSolarRechargeEnergy : 0 ,
cumulatedSolarRechargeEnergy : 0 ,
totalProducedSolarEnergy : 0 ,
totalProducedSolarEnergy : 0 ,
cumulatedMotorConsumption : 0
cumulatedMotorConsumption : 0 ,
cumulatedHumanEnergy : 0 ,
cumulatedDistance : 0 ,
flatTerrainSpeed : 0 ,
uphillSpeed : 0 ,
downhillSpeed : 0 ,
averageSpeed : 0
} ;
} ;
let remainingBatteryCharge = vehicle . batteryCapacity ;
let remainingBatteryCharge = vehicle . batteryCapacity ;
let outing : Outing = { distance : 0 , ascendingElevation : 0 } ;
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 day = 0 ; day < 365 ; ++ day ) {
for ( let hour = 0 ; hour < 24 ; ++ hour ) {
for ( let hour = 0 ; hour < 24 ; ++ hour ) {
@ -119,14 +174,20 @@ namespace Simulator {
planning . getOuting ( day % 7 , hour , outing ) ;
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)
let production = vehicle . solarPower ( solarIrradiance [ hourIdx ] ) * 1.0 ; // produced energy in Wh is equal to power (W) multiplied by time (h)
result . totalProducedSolarEnergy += production ;
result . totalProducedSolarEnergy += production ;
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
// 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 ;
let fullGridRecharge = false ;
if ( remainingBatteryCharge > vehicle . batteryCapacity ) {
if ( remainingBatteryCharge > vehicle . batteryCapacity ) {
@ -142,7 +203,7 @@ namespace Simulator {
result . gridChargeCount += 1 ;
result . gridChargeCount += 1 ;
}
}
result . cumulatedMotorConsumption += consumption ;
result . cumulatedMotorConsumption += consumption . motorEnergy ;
result . cumulatedSolarRechargeEnergy += solarCharge ;
result . cumulatedSolarRechargeEnergy += solarCharge ;
result . batteryLevel [ hourIdx ] = fullGridRecharge ? 0 : remainingBatteryCharge ;
result . batteryLevel [ hourIdx ] = fullGridRecharge ? 0 : remainingBatteryCharge ;