Vehicle energy consumption and production simulator
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

239 lines
9.8 KiB

namespace Simulator {
interface ConsumptionData {
motorEnergy: number;
humanEnergy: number;
averageSpeed: number;
}
export class Vehicle {
batteryCapacity: number;
batteryEfficiency: number = 0.9;
gridTransformerEfficiency: number = 0.85;
rechargeMargin: number = 0.15; // We recharge the battery before an outing if it would otherwise go below this charge ratio
solarPanelEfficiency: number = 0.15;
solarPanelArea: number = 1.0; // in square meters
emptyVehicleWeight: number = 80; // kg
driverWeight: number = 60; // kg
additionalWeight: number = 0; // additional weight, not counting cyclist and empty vehicle weight, in kg
humanPower: number = 100; // W
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
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)
potentialEnergy = potentialEnergy / 3600; // convert joules to watt-hour
// empirical measures
let baseConsumption = 13; // in Wh/km, when human power is 0
let additionalConsumptionPerKg = 0.01; // in Wh/km per kg of total vehicle weight (additional losses due to increased friction, mostly independent of speed)
let requiredEnergy = Math.max(0, distance * (baseConsumption + totalWeight * additionalConsumptionPerKg) + potentialEnergy);
let motorPowerLimit = this.nominalMotorPower;
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")
if(actualSpeed > this.assistanceSpeedLimit) {
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 accounting for assistance speed limit: " + (Math.round(actualSpeed*10)/10) + " km/h (motor power limited to: " + Math.round(motorPowerLimit) + " W)")
}
if(actualSpeed > this.speedLimit) {
actualSpeed = this.speedLimit;
tripDuration = distance / actualSpeed;
motorPowerLimit = Math.max(0, requiredEnergy/tripDuration - this.humanPower);
}
let humanEnergy = Math.max(requiredEnergy, tripDuration * this.humanPower);
inOutConsumption.motorEnergy += Math.max(motorPowerLimit * tripDuration, requiredEnergy - humanEnergy);
inOutConsumption.humanEnergy += humanEnergy;
inOutConsumption.averageSpeed += actualSpeed;
}
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, public flatTerrainRatio: 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.
cumulatedHumanEnergy: number;
cumulatedDistance: number;
flatTerrainSpeed: number;
uphillSpeed: number;
downhillSpeed: number;
averageSpeed: number;
outOfBatteryDistance: number; // distance, in km, that the driver had to pedal without assistance, because the battery was empty
}
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,
cumulatedHumanEnergy: 0,
cumulatedDistance: 0,
flatTerrainSpeed: 0,
uphillSpeed: 0,
downhillSpeed: 0,
averageSpeed: 0,
outOfBatteryDistance: 0
};
let remainingBatteryCharge = vehicle.batteryCapacity;
let outing: Outing = { distance: 0, ascendingElevation: 0 };
let consumption: ConsumptionData = { motorEnergy: 0, humanEnergy: 0, averageSpeed: 0 };
let resetConsumption = function(outConsumption: ConsumptionData) {
outConsumption.motorEnergy = 0; outConsumption.humanEnergy = 0; outConsumption.averageSpeed = 0;
};
let flatTerrainRatio = MathUtils.clamp(planning.flatTerrainRatio, 0.0, 1.0);
if(planning.dailyAscendingElevation <= 0) flatTerrainRatio = 1.0;
let flatDistance = planning.dailyDistance * flatTerrainRatio;
resetConsumption(consumption);
vehicle.consumption(flatDistance, 0, consumption);
result.flatTerrainSpeed = consumption.averageSpeed;
let uphillDistance = planning.dailyDistance * (1.0 - flatTerrainRatio) * 0.5;
resetConsumption(consumption);
vehicle.consumption(uphillDistance, planning.dailyAscendingElevation, consumption);
result.uphillSpeed = consumption.averageSpeed;
let downhillDistance = planning.dailyDistance * (1.0 - flatTerrainRatio) * 0.5;
resetConsumption(consumption);
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) {
let hourIdx = day * 24 + hour;
planning.getOuting(day % 7, hour, outing);
resetConsumption(consumption);
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;
let remainingBatteryChargeBeforeOuting = remainingBatteryCharge;
remainingBatteryCharge += solarCharge - consumption.motorEnergy;
let gridRechargeFrom = -1;
let lowBatteryThreshold = vehicle.rechargeMargin * vehicle.batteryCapacity;
if(remainingBatteryCharge > vehicle.batteryCapacity) {
solarCharge -= remainingBatteryCharge - vehicle.batteryCapacity;
remainingBatteryCharge = vehicle.batteryCapacity;
}
else if(remainingBatteryCharge <= lowBatteryThreshold || (day==364 && hour==23)) {
gridRechargeFrom = remainingBatteryChargeBeforeOuting;
let rechargeEnergy = vehicle.batteryCapacity - remainingBatteryChargeBeforeOuting;
remainingBatteryCharge += rechargeEnergy;
result.cumulatedGridRechargeEnergy += rechargeEnergy;
result.gridChargeCount += 1;
if(remainingBatteryCharge < 0)
{
// battery was exhausted during outing
let missingEnergy = -remainingBatteryCharge;
result.outOfBatteryDistance += outing.distance * missingEnergy / consumption.motorEnergy;
consumption.motorEnergy -= missingEnergy;
consumption.humanEnergy += missingEnergy;
// charge battery again after outing
let secondRechargeEnergy = vehicle.batteryCapacity;
remainingBatteryCharge = vehicle.batteryCapacity;
result.cumulatedGridRechargeEnergy += secondRechargeEnergy;
result.gridChargeCount += 1;
gridRechargeFrom = 0;
}
}
result.cumulatedMotorConsumption += consumption.motorEnergy;
result.cumulatedSolarRechargeEnergy += solarCharge;
result.batteryLevel[hourIdx] = gridRechargeFrom >= 0 ? gridRechargeFrom : remainingBatteryCharge;
}
}
return result;
}
}