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.
128 lines
5.2 KiB
128 lines
5.2 KiB
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; |
|
} |
|
}
|
|
|